frontendadapter

package
v0.3.9 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: GPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package frontendadapter registers one generic Ether frontend per plugin-provided protocol surface (spec §6.5). A plugin cannot register Go code, so Ether mounts a reverse proxy that turns each HTTP request into a plugins.Manager.CallStream call, authenticating the viewer through Ether first and injecting only the viewer-identity headers the spec defines. The plugin never sees an Ether credential.

Index

Constants

View Source
const (
	// DefaultRequestsPerMinute is the per-adapter, per-viewer token-bucket
	// budget when a declaration does not override it.
	DefaultRequestsPerMinute = 600
	// MaxRequestBodyBytes caps an inbound proxied body.
	MaxRequestBodyBytes = 1 << 20
	// MaxHostedResponseBytes caps a hosted (wasm) plugin's response body.
	// The hosted runtime answers small protocol documents through Call;
	// media bytes belong on Ether stream URLs instead.
	MaxHostedResponseBytes = 10 << 20
)

Defaults for the isolation limits in spec §6.5.

View Source
const (
	AuthEtherToken = "ether_token"
	AuthBasic      = "basic"
	AuthNoneLAN    = "none_lan"
)

Auth modes an adapter declaration may pick (spec §6.5).

View Source
const (
	HeaderViewer     = "X-Ether-Viewer"
	HeaderViewerName = "X-Ether-Viewer-Name"
	HeaderLibraries  = "X-Ether-Libraries"
)

Viewer-identity headers the proxy injects (spec §6.5). The plugin never sees an Ether credential; these are the only Ether-issued identity values it receives.

View Source
const DefaultStreamTicketTTL = 2 * time.Minute

DefaultStreamTicketTTL is the lifetime of a minted stream ticket.

View Source
const KindMediaPath = "/ether/v1/kind-media/"

KindMediaPath is the Ether-owned endpoint a plugin embeds in an adapter response (spec §6.5). The path segment after it is the opaque ticket.

Variables

View Source
var ErrTicketInvalid = errors.New("frontendadapter: stream ticket invalid")

ErrTicketInvalid means the ticket is unknown, expired or out of scope.

Functions

func AdapterID

func AdapterID(pluginID, adapterKey string) frontend.Protocol

AdapterID builds the frontend protocol id for one adapter declaration: "plugin:<pluginID>:<adapterKey>". It is stable across mounts so an owner's Apps-page settings survive enable/disable cycles.

Types

type Catalog

type Catalog interface {
	// EnabledAdapters returns one Declaration per adapter of every enabled
	// plugin. A disabled, removed or uninstalled plugin contributes none.
	EnabledAdapters(ctx context.Context) ([]Declaration, error)
}

Catalog reports the adapter declarations of currently installed, enabled plugins. App wiring implements it from the plugin store; the controller never touches the store itself.

type Controller

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

Controller keeps the registry's adapter frontends in step with the installed plugin set: adapters register on enable and unregister on disable/remove, without restarting Ether (spec §6.5).

func NewController

func NewController(reg Registry, mgr Manager, catalog Catalog) (*Controller, error)

NewController builds the lifecycle controller.

func (*Controller) Disable

func (c *Controller) Disable(ctx context.Context, pluginID string) error

Disable unmounts and unregisters every adapter of one plugin.

func (*Controller) Enable

func (c *Controller) Enable(ctx context.Context, decls []Declaration, mount bool) error

Enable registers (and mounts, if the owner enabled it) the adapters of one plugin. It is idempotent.

func (*Controller) Live

func (c *Controller) Live() []frontend.Protocol

Live reports the currently registered adapter protocols, sorted.

func (*Controller) Sync

func (c *Controller) Sync(ctx context.Context) error

Sync reconciles registered adapters with the catalog: new adapters are registered, adapters whose plugin is gone are unregistered. It is called after every install, enable, disable, upgrade and remove.

type Declaration

type Declaration struct {
	PluginID string
	Meta     plugins.FrontendAdapterMeta
	// Name and Icon come from the plugin manifest (spec §6.5). Name is the
	// adapter label; Icon is the plugin logo when the adapter has none.
	Name string
	Icon string
	// DocsURL and Version are surfaced on the descriptor for the Apps page.
	DocsURL string
	Version string
	// SettingsSchema is the adapter's (or the plugin's) settings schema, if
	// any, forwarded verbatim to the descriptor.
	SettingsSchema any
	// Libraries lists the library ids this adapter publishes. The proxy
	// intersects them with the viewer's visible libraries: a request that
	// would expose a library the viewer cannot see answers 404, never 403.
	// Empty means the adapter is not library-scoped (the viewer still must
	// authenticate) and X-Ether-Libraries is empty.
	Libraries []core.LibraryID
}

Declaration is everything the generic frontend needs from one contributes.frontendAdapters[] entry plus its owning plugin.

type Frontend

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

Frontend is the frontend.Frontend implementation for one adapter. It is registered on plugin enable and unregistered on disable/remove.

func New

func New(decl Declaration, mgr Manager) (*Frontend, error)

New builds the generic frontend for one adapter declaration.

func (*Frontend) Describe

func (f *Frontend) Describe() frontend.Descriptor

Describe reports the descriptor Ether's Apps page renders (spec §6.5).

func (*Frontend) ID

func (f *Frontend) ID() frontend.Protocol

ID returns the frontend protocol id.

func (*Frontend) Mount

func (f *Frontend) Mount(ctx context.Context, host frontend.Host) (frontend.Instance, error)

Mount binds the adapter to the shared listener under its path prefix (or its own port when the owner enabled defaultPort) and returns the proxy.

type Manager

type Manager interface {
	CallStream(ctx context.Context, pluginID string, req plugins.Request) (plugins.Response, io.ReadCloser, error)
	Call(ctx context.Context, pluginID string, req plugins.Request) (plugins.Response, error)
}

Manager is the narrow slice of plugins.Manager the proxy uses. It is an interface so tests can supply a fake and so the adapter cannot reach any other Manager method. CallStream relays bodies (remote/tunnel plugins); Call serves small buffered responses and is the hosted-plugin fallback.

type MediaDeps

type MediaDeps struct {
	// Tickets redeems the scoped, short-lived grant.
	Tickets *TicketIssuer
	// Viewer loads the current policy user by id, so the library and rating
	// checks use live restrictions rather than a stale ticket snapshot.
	Viewer func(ctx context.Context, id int64) (*core.Viewer, error)
	// Open opens one plugin item ref for the granted viewer, honouring the
	// request's accepted MIME types. It runs the same authorization path
	// kind playback uses.
	Open func(ctx context.Context, viewer *core.Viewer, itemRef string, accept []string) (*frontend.KindMedia, error)
}

MediaDeps are the seams the kind-media endpoint needs. The app fills them from pluginitems + the kind resolver; the endpoint never touches tables.

type MediaHandler

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

MediaHandler serves /ether/v1/kind-media/{ticket}: it redeems the ticket, loads the viewer, opens the item through KindStreams and streams the bytes with Range support. The ticket is the only credential the request needs.

func NewMediaHandler

func NewMediaHandler(deps MediaDeps) *MediaHandler

NewMediaHandler builds the endpoint handler. It returns nil when the dependencies are incomplete, so app wiring can leave the route unmounted.

func (*MediaHandler) ServeHTTP

func (h *MediaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Registry

type Registry interface {
	Register(frontend.Frontend) error
	Unregister(frontend.Protocol) error
	Mount(ctx context.Context, id frontend.Protocol) (frontend.Instance, error)
	Unmount(ctx context.Context, id frontend.Protocol) error
	Instance(id frontend.Protocol) (frontend.Instance, bool)
}

Registry is the slice of frontend.Registry the controller drives. It is an interface so the lifecycle is testable without a real registry.

type StreamGrant

type StreamGrant struct {
	ViewerID int64
	ItemID   string
	Adapter  string
}

StreamGrant is the scope a stream ticket authorizes.

type TicketIssuer

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

TicketIssuer mints and validates scoped, short-lived stream tickets for one adapter. It is safe for concurrent use. App wiring mounts Validate at the kind-media endpoint; the SDK helper builds the URL.

func NewTicketIssuer

func NewTicketIssuer() *TicketIssuer

NewTicketIssuer builds an issuer with the default TTL.

func (*TicketIssuer) Issue

func (t *TicketIssuer) Issue(grant StreamGrant) (string, error)

Issue mints a ticket for the given grant and returns its opaque token.

func (*TicketIssuer) Redeem

func (t *TicketIssuer) Redeem(token string) (StreamGrant, error)

Redeem returns the grant a live ticket authorizes, or ErrTicketInvalid if it is unknown or expired. The kind-media endpoint uses it: the ticket itself is the proof (short-lived and unguessable), so the request carries no separate credential. The grant's viewer and item scope what may be opened.

func (*TicketIssuer) SetTTL

func (t *TicketIssuer) SetTTL(ttl time.Duration)

SetTTL overrides the ticket lifetime (tests use a short TTL).

func (*TicketIssuer) StreamURL

func (t *TicketIssuer) StreamURL(grant StreamGrant) (string, error)

StreamURL mints a ticket for grant and returns the Ether stream URL the plugin should embed in its adapter response. The URL is served by Ether from KindMedia, so bytes stay off the plugin path even for remote plugins.

func (*TicketIssuer) Sweep

func (t *TicketIssuer) Sweep()

Sweep drops expired tickets; the host may call it periodically.

func (*TicketIssuer) Validate

func (t *TicketIssuer) Validate(token string, viewerID int64, itemID, adapter string) (StreamGrant, error)

Validate checks that token is live and matches the presented scope. It does not consume the ticket: a media URL may be ranged many times within its short lifetime. An expired or out-of-scope ticket returns ErrTicketInvalid and is forgotten.

Jump to

Keyboard shortcuts

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