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
- Variables
- func AdapterID(pluginID, adapterKey string) frontend.Protocol
- type Catalog
- type Controller
- type Declaration
- type Frontend
- type Manager
- type MediaDeps
- type MediaHandler
- type Registry
- type StreamGrant
- type TicketIssuer
- func (t *TicketIssuer) Issue(grant StreamGrant) (string, error)
- func (t *TicketIssuer) Redeem(token string) (StreamGrant, error)
- func (t *TicketIssuer) SetTTL(ttl time.Duration)
- func (t *TicketIssuer) StreamURL(grant StreamGrant) (string, error)
- func (t *TicketIssuer) Sweep()
- func (t *TicketIssuer) Validate(token string, viewerID int64, itemID, adapter string) (StreamGrant, error)
Constants ¶
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.
const ( AuthEtherToken = "ether_token" AuthBasic = "basic" AuthNoneLAN = "none_lan" )
Auth modes an adapter declaration may pick (spec §6.5).
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.
const DefaultStreamTicketTTL = 2 * time.Minute
DefaultStreamTicketTTL is the lifetime of a minted stream ticket.
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 ¶
var ErrTicketInvalid = errors.New("frontendadapter: stream ticket invalid")
ErrTicketInvalid means the ticket is unknown, expired or out of scope.
Functions ¶
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.
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).
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 ¶
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.