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

pluginkit

package
v0.48.1 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package pluginkit is the guest SDK for filex app plugins written in Go.

A plugin is an ordinary Go program compiled for WebAssembly:

GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm .

It registers what it offers with Run (from init(), see there) and never blocks: filex calls its exports (describe, action_run, view_event, page_event, tick, on_event) one at a time, on a fresh instance per call, and the plugin answers through the host functions filex hands it (files, progress, settings, engines, …), each one gated by a permission the administrator granted at install.

The wire shapes live in pluginkit/wire; a plugin that only needs describe and one action needs nothing else from here than Run and the helpers.

Index

Constants

View Source
const ChunkSize = 1 << 20

ChunkSize is the largest block one file_read/file_write moves.

View Source
const LockUntilLifted = -1

LockUntilLifted is FileLock's ttlDays for "no end": the lock holds until the app lifts it or an administrator does (audited).

Variables

View Source
var ErrNotRegistered = errors.New("pluginkit: no plugin registered (call pluginkit.Run in main)")

ErrNotRegistered is returned when an export is called before Run.

View Source
var ErrNotWasm = errors.New("pluginkit: host functions are only available inside filex (GOOS=wasip1)")

ErrNotWasm is what every host call returns off-wasm.

Functions

func EngineAvailable

func EngineAvailable(name string) bool

EngineAvailable reports whether the engine is installed AND granted.

func FileLock

func FileLock(ref string, ttlDays int, reason string) (time.Time, error)

FileLock freezes a file read-only for everyone — administrators included — for ttlDays (0 = 30, at most 365; LockUntilLifted = no end) or until FileUnlock. Folder renames/moves/deletes that would carry the file along are refused too. The locking plugin's own jobs may still write into the file. Action jobs only. Returns the expiry (zero for LockUntilLifted).

ref may be an input, or one of this job's OWN outputs: that lock is promised and taken when the job's output is committed (never, if the job fails) — how an app locks the file it has just produced.

func FileLockMessage

func FileLockMessage(ref string, ttlDays int, key string, args map[string]string) (time.Time, error)

FileLockMessage is FileLock with its reason named by one of the manifest's `messages` (key) and that message's {placeholders} (args): filex then says the reason in each reader's own language — the administrator's panel, the details panel, a refused rename — instead of in the language of the call that took the lock. Prefer it to FileLock's plain string.

func FileLockPath

func FileLockPath(qualified string, ttlDays int, reason string) (time.Time, error)

FileLockPath is FileLock by adapter-qualified path (`docs://a/b.pdf`) on the job's own storage — for a file that is not one of the job's inputs.

func FileUnlock

func FileUnlock(ref string) error

FileUnlock lifts this plugin's lock on an input (no-op when unlocked).

func FileUnlockPath

func FileUnlockPath(qualified string) error

FileUnlockPath is FileUnlock by adapter-qualified path.

func HostSign

func HostSign(keyRef string, digest []byte) ([]byte, error)

HostSign signs a sha256 digest with the key behind keyRef; the answer is a DER-encoded ECDSA signature (ecdsa-p256-sha256).

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is the host saying the thing asked about does not exist — for ShareInfo, a link that was deleted outright.

func IsPermissionDenied

func IsPermissionDenied(err error) bool

IsPermissionDenied reports whether err is a refused host call.

func KeyDestroy

func KeyDestroy(keyRef string) error

KeyDestroy discards the private key behind keyRef; the certificate stays.

func Log

func Log(level string, msg string)

Log on a non-wasm build (tests, `go vet` on the host) goes to the standard logger, so a plugin's unit tests can run natively.

func MailSend

func MailSend(to, subject, body string) error

MailSend sends a plain-text mail through filex's SMTP (permission `mail:send`, 60 per hour per plugin; filex appends its own footer).

func MailSendIn

func MailSendIn(lang, to, subject, body string) error

MailSendIn is MailSend for a mail the app wrote in lang (a language tag: "en", "tr", "es"…). filex writes its footer ("Sent by the … app on filex") in that language — a language pack's included — and labels the message with it (Content-Language). MailSend leaves both to the language the call runs in, which is empty on a scheduled wake-up: a reminder the app writes in the requester's language would then carry the instance's footer.

func NotifySend

func NotifySend(n Notice) (int64, error)

NotifySend raises a `plugin.notice` notification (permission `notify:send`).

func OpenFile

func OpenFile(path, action, view string) *wire.OpenRequest

OpenFile is the answer a row action gives when clicking it should take the person to a document:

&wire.Surface{Open: pluginkit.OpenFile(path, "fill", "")}

Name an action OR a view, or neither to just open the file. The host checks the screen belongs to this plugin, and filex drops the link when the person asking may not see that file — so a list can offer to go somewhere without becoming a way around permissions.

func Progress

func Progress(done, total int64, message string)

Progress reports done/total (any unit; total 0 = unknown) and an optional message the tray shows.

func PublicPageRevoke deprecated

func PublicPageRevoke(token string) error

Deprecated: use ShareRevoke.

func PublicPageState deprecated

func PublicPageState(token string, out any) error

Deprecated: use ShareState.

func PublicPageStateSet deprecated

func PublicPageStateSet(token string, state any) error

Deprecated: use ShareStateSet.

func ReadInput

func ReadInput(ref string) ([]byte, error)

ReadInput reads a whole input into memory.

func Run

func Run(p *Plugin)

Run registers the plugin.

⚠ Call it from an init() function, not from main(). A wasip1 module built with -buildmode=c-shared is a reactor: filex calls the exports directly, package initialisers run, main() does not. A plugin that registers in main() answers every call with ErrNotRegistered.

func Setting

func Setting(key string) (string, bool, error)

Setting reads one admin-configured setting (permission `settings`); secret fields are only readable here.

func SharePIN

func SharePIN(token string) (pin string, reason string, err error)

SharePIN reads the PIN of one of this app's own links, for the PERSON making the call (permission `public_pages`).

⚠⚠ This is the platform's own PIN store, not a copy: an app must never keep a PIN of its own. The host allows the read only to the person who created the link or to an administrator, and writes an audit row for every read, successful or not — the same row the "My shares" screen writes. A call with no signed-in person behind it (a public page's visitor, the hourly wake-up) is refused.

An empty PIN comes back with a REASON rather than as a blank:

no_pin           the link is not PIN-protected
no_secret_key    this installation cannot show any PIN it minted
not_recoverable  this particular PIN is gone for good

Say the reason. A blank where a secret should be reads as a bug, and the person cannot tell "there is no PIN" from "we lost it".

func ShareRevoke

func ShareRevoke(token string) error

ShareRevoke ends a share link early.

func ShareState

func ShareState(token string, out any) error

ShareState reads the link's durable record into out. Inside a page_event call token may be "" (the current link).

func ShareStateSet

func ShareStateSet(token string, state any) error

ShareStateSet replaces the link's durable record (≤ 64 KiB).

func StateDelete

func StateDelete(ref, key string) error

StateDelete removes one key.

func StateGet

func StateGet(ref, key string) (string, bool, error)

StateGet reads per-file state kept for an input ref (permission `state`).

func StateSet

func StateSet(ref, key, value string) error

StateSet writes per-file state (≤ 64 KiB); action jobs only.

func WriteOutput

func WriteOutput(name string, b []byte) (wire.OutputRef, error)

WriteOutput creates an output and writes b into it in one go.

Types

type ActionFunc

type ActionFunc func(in *wire.ActionRunInput) (*wire.ActionRunOutput, error)

ActionFunc runs one action. It returns the outputs it created through CreateOutput plus an optional message; returning an error fails the job with that message.

type Asset

type Asset struct {
	// Ref reads it: OpenInput(Ref) / ReadInput(Ref). No `files:read` needed.
	Ref  string `json:"ref"`
	Size int64  `json:"size"`
	// Cached is true when no network was used (it was already downloaded).
	Cached bool `json:"cached"`
}

Asset is a file asset_fetch put in this app's cache on the host.

func AssetFetch

func AssetFetch(url, sha256 string, maxBytes int64) (*Asset, error)

AssetFetch downloads url ONCE into this app's cache on the host and hands back a ref to read it with. The file must match sha256 (64 hex, pinned by the app) — a mismatch is refused with the `integrity` code and nothing is kept — and may be at most maxBytes (the host's ceiling is 32 MiB). Later calls with the same sha256 are served from the cache without the network.

The URL's host must be one of the app's `http:<host>` permissions, like HTTPDo; https only. Offline, or when the host cannot reach the server, the call fails (`unavailable` / `timeout`) and the app decides what to do — the signing app tells the person which characters will not print.

type EngineRequest

type EngineRequest struct {
	Engine   string            `json:"engine"`
	Args     []string          `json:"args"`
	Inputs   map[string]string `json:"inputs,omitempty"`
	Outputs  []string          `json:"outputs,omitempty"`
	TimeoutS int               `json:"timeout_s,omitempty"`
}

EngineRequest runs one of the host's heavy engines (permission `engines:<name>`). Inputs maps a file name inside the run directory to a ref; Args are bare tokens (no paths); Outputs names the files to collect (empty = every new file).

type EngineResult

type EngineResult struct {
	Exit       int              `json:"exit"`
	StdoutTail string           `json:"stdout_tail"`
	StderrTail string           `json:"stderr_tail"`
	Outputs    []wire.OutputRef `json:"outputs"`
	DurationMS int64            `json:"duration_ms"`
}

EngineResult is what the engine produced.

func EngineRun

func EngineRun(req EngineRequest) (*EngineResult, error)

EngineRun runs the engine and returns its artefacts as refs the plugin may read (OpenInput) or hand back as outputs.

type HTTPRequest

type HTTPRequest struct {
	Method   string            `json:"method,omitempty"`
	URL      string            `json:"url"`
	Headers  map[string]string `json:"headers,omitempty"`
	Body     []byte            `json:"-"`
	BodyB64  string            `json:"body_b64,omitempty"`
	TimeoutS int               `json:"timeout_s,omitempty"`
}

HTTPRequest is an outbound call; the host must be in the manifest's `http:<host>` grants, private/local addresses are refused, bodies are capped at 8 MiB and the call at 30 s.

type HTTPResponse

type HTTPResponse struct {
	Status  int               `json:"status"`
	Headers map[string]string `json:"headers"`
	BodyB64 string            `json:"body_b64"`
	Body    []byte            `json:"-"`
}

HTTPResponse is the answer; Body is decoded for the caller.

func HTTPDo

func HTTPDo(req HTTPRequest) (*HTTPResponse, error)

HTTPDo performs the request.

type HostError

type HostError struct {
	Code    string
	Message string
}

HostError is a host function's in-band failure.

func (*HostError) Error

func (e *HostError) Error() string

type HostSigner

type HostSigner struct {
	KeyRef string
	Cert   *x509.Certificate
	// contains filtered or unexported fields
}

HostSigner is a crypto.Signer over a host-held key.

func NewHostSigner

func NewHostSigner(issued *IssuedCert) (*HostSigner, error)

NewHostSigner parses the issued certificate and binds the key ref.

func (*HostSigner) Public

func (h *HostSigner) Public() crypto.PublicKey

Public returns the certificate's public key.

func (*HostSigner) Sign

func (h *HostSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)

Sign asks the host to sign digest (must be a sha256 digest).

type Input

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

Input is an open handle on one of the call's inputs. It implements io.Reader; Close releases the host side.

func OpenInput

func OpenInput(ref string) (*Input, error)

OpenInput opens a file ref from ActionRunInput.Inputs (or a ref an engine produced) for reading.

func (*Input) Close

func (in *Input) Close() error

Close releases the handle.

func (*Input) Read

func (in *Input) Read(p []byte) (int, error)

func (*Input) Size

func (in *Input) Size() int64

Size is the file's size as the host reported it (-1 when unknown).

type IssuedCert

type IssuedCert struct {
	KeyRef   string `json:"key_ref"`
	CertPEM  string `json:"cert_pem"`
	ChainPEM string `json:"chain_pem"`
	NotAfter string `json:"not_after"`
}

IssuedCert is a leaf certificate the host minted for one signer.

func CertIssue

func CertIssue(commonName, email string, days int) (*IssuedCert, error)

CertIssue mints a certificate for commonName (the signer's name), with an optional e-mail SAN, valid for days (host-clamped). Action jobs only.

func PlatformSeal

func PlatformSeal() (*IssuedCert, error)

PlatformSeal hands out the installation's SEAL for this app: a certificate "filex document seal" (OU = the app's name) from the tenant's authority, with a key the host keeps. Unlike CertIssue it is the same key on every call — until the authority is rotated — and KeyDestroy refuses it. Sign with it through NewHostSigner, exactly like a signer's certificate. Action jobs only; permission `sign`.

It exists for one thing: an app that finishes a document on the installation's behalf (the signing app seals every completed request, so the bytes every party is sent a hash of were closed by filex itself).

type Notice

type Notice struct {
	Title    wire.Text      `json:"title"`
	Body     wire.Text      `json:"body,omitempty"`
	Severity string         `json:"severity,omitempty"`
	Meta     map[string]any `json:"meta,omitempty"`
	// ToUserID addresses one person (their bell, push and — if they turned
	// it on — mail). 0 posts to the instance feed as before.
	ToUserID int64 `json:"to_user_id,omitempty"`
	// Target makes the notification clickable: a click opens the file and,
	// when Action or View is set, that screen of this plugin on it.
	Target *NoticeTarget `json:"target,omitempty"`
}

Notice is what NotifySend posts: title/body per language (en required), severity info|warning|error, and a few small facts.

type NoticeTarget

type NoticeTarget struct {
	Ref     string `json:"ref,omitempty"`
	Path    string `json:"path,omitempty"`
	Action  string `json:"action,omitempty"`
	View    string `json:"view,omitempty"`
	Section string `json:"section,omitempty"`
}

NoticeTarget names the file (an input Ref, or an adapter-qualified Path on the job's storage) and the optional screen to open on it.

A notice may instead open one of this app's HOME pages: no Ref, no Path, View naming a view placed `home`, and optionally the Section of that page to land on (Surface.Sections). A click then opens the page itself, in the app, at that section — for a notice about the list rather than about one document ("your request progressed" opens the requests you sent).

type Output

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

Output is a file the plugin is producing. Name it in ActionRunOutput.Outputs (by Ref) to have filex store it.

func CreateOutput

func CreateOutput(name string) (*Output, error)

CreateOutput opens a new output file (files:write).

func (*Output) Close

func (o *Output) Close() error

Close finishes the file; call it before returning the action.

func (*Output) Ref

func (o *Output) Ref() wire.OutputRef

Ref is the handle to name in ActionRunOutput.Outputs.

func (*Output) Write

func (o *Output) Write(p []byte) (int, error)

type PageCreate

type PageCreate struct {
	// PageID names the manifest page this link renders. EMPTY opens an
	// ordinary download share of the document instead — no surface, no
	// copies, the kind an administrator already revokes in Shares.
	PageID string `json:"page_id"`
	// Ref / Path name the document the link is about; empty means the call's
	// first input. ⚠ The host has read these since v3 and both documents
	// promise them, but this struct did not carry them — so a plugin could
	// not name any file but the first input, which is exactly what a
	// page-less link's refusal message asks it to do.
	Ref       string           `json:"ref,omitempty"`
	Path      string           `json:"path,omitempty"`
	Subject   string           `json:"subject,omitempty"`
	PIN       string           `json:"pin,omitempty"`
	TTLDays   int              `json:"ttl_days,omitempty"`
	MaxVisits int              `json:"max_visits,omitempty"`
	State     any              `json:"state,omitempty"`
	Files     []wire.OutputRef `json:"files,omitempty"`
	// Purpose says what THIS link is, in a list of links (My shares, the
	// admin's Shares) — the same shape a manifest page declares, and it wins
	// over the page's own. ⚠ A page-less link (no PageID: a plain share of a
	// file, e.g. a finished document handed to everybody) has no page to
	// declare one, so this is the only way it can say "Signed copy" instead
	// of reading as a share somebody made by hand.
	Purpose *wire.PagePurpose `json:"purpose,omitempty"`
}

PageCreate is what ShareCreate asks for. PIN: "auto" mints a 6-digit PIN (returned to the app once; the link's creator or an administrator can read it back later, as for any share), a literal sets it, "" leaves the page open — within the manifest's `pin` policy for that page. Files are refs of this call (inputs, outputs, engine artefacts) copied out for the visitor.

type PageCreated

type PageCreated struct {
	Token     string `json:"token"`
	URL       string `json:"url"`
	PIN       string `json:"pin,omitempty"`
	ExpiresAt string `json:"expires_at"`
}

PageCreated is the answer: the link, the PIN (once, when one was made).

func PublicPageCreate deprecated

func PublicPageCreate(req PageCreate) (*PageCreated, error)

The v1/v2 spelling. An app's public page is a share from v3 on; these names keep older sources compiling and call the same host functions.

Deprecated: use ShareCreate.

func ShareCreate

func ShareCreate(req PageCreate) (*PageCreated, error)

ShareCreate opens a share link, /s/<token>, for an outside participant (permission `public_pages`; action jobs only).

type PageFunc

type PageFunc func(in *wire.ViewEventInput) (*wire.Surface, error)

PageFunc answers a public-page event.

type Plugin

type Plugin struct {
	// Manifest is echoed by describe. It must equal the filex-app.json the
	// administrator installed (name, version, permissions); filex refuses the
	// plugin otherwise.
	Manifest wire.Manifest
	Actions  map[string]ActionFunc
	Views    map[string]ViewFunc
	Pages    map[string]PageFunc
	// Tick is the hourly wake-up. It is called only when the manifest asks
	// for the `schedule` permission AND the administrator granted it; an
	// app that leaves this nil but asks for the permission answers every
	// wake-up with an error, which lands in its log in the admin panel.
	Tick TickFunc
	// UI answers the app's own interface, by method name (`engine.call`).
	// An interface-only app has no module at all; an app with both registers
	// here what its interface may ask the module.
	UI map[string]UICallFunc
}

Plugin is what a program registers with Run.

type ShareFacts

type ShareFacts struct {
	Page    string `json:"page"`
	Subject string `json:"subject"`
	Visits  int    `json:"visits"`
	// ExpiresAt is when the link stops (nil: never). Revoking a link sets it
	// to the moment of the revoke, so a link that ends EARLIER than the day
	// the app was handed at share_create was ended by somebody.
	ExpiresAt *time.Time `json:"expires_at"`
	// Revoked says the link no longer opens: it ran out, reached its visit
	// ceiling, or somebody revoked it. ExpiresAt tells the three apart from
	// what the app recorded when it opened the link.
	Revoked bool `json:"revoked"`
	// HasPIN: the link is PIN-protected at all.
	HasPIN bool `json:"has_pin"`
	// PinRecoverable: the PIN can still be SHOWN to the person who made the
	// link (SharePIN). False on a link minted before the instance kept a
	// recoverable copy, on an instance with no secret key, and on one whose
	// key has been rotated away — the link still works, its PIN is simply
	// gone. ⚠ Offer "show the PIN" only when this is true; otherwise say so
	// and offer what does work, which is a new link.
	PinRecoverable bool `json:"pin_recoverable"`
}

ShareFacts is what an app may learn about one of its OWN links: never the token's owner, the PIN or who else it went to.

func ShareInfo

func ShareInfo(token string) (*ShareFacts, error)

ShareInfo reads the facts of one of this app's links (permission `public_pages`). Allowed in every call — a screen, a page and the hourly wake-up may read, which is how an app learns that a link it handed out was revoked from the Shares screen: nobody tells it; it asks.

A link that was DELETED outright (an administrator's Delete, not Revoke) answers an error for which IsNotFound is true. Treat it as ended.

type SignInfo

type SignInfo struct {
	Available bool   `json:"available"`
	Reason    string `json:"reason,omitempty"`
	CACertPEM string `json:"ca_cert_pem,omitempty"`
	// CACertsPEM is every authority this tenant has signed with, retired
	// ones included, as one PEM bundle. Build a verifier's root pool from
	// THIS, not from CACertPEM: trusting only the live authority calls
	// every signature made before the last rotation untrusted.
	CACertsPEM string `json:"ca_certs_pem,omitempty"`
	Algorithm  string `json:"algorithm,omitempty"`
}

SignInfo says whether signing is available and hands out the CA cert.

func HostSignInfo

func HostSignInfo() (*SignInfo, error)

HostSignInfo asks the host whether it can sign for this call's tenant.

type StateItem

type StateItem struct {
	// Path is adapter-qualified (`docs://reports/nda.pdf`) — what a menu
	// action or a view takes.
	Path  string `json:"path"`
	Name  string `json:"name"`
	Key   string `json:"key"`
	Value string `json:"value"`
}

StateItem is one file a StateList answer names.

func StateList

func StateList(key string, limit int) ([]StateItem, error)

StateList finds the files this plugin keeps `key` on (empty key = every key), newest first. This is how a home screen lists its own work: without it an app can only see the file it was opened on. Deleted files are not in the answer, and neither are files the person asking may not see.

type TickFunc

type TickFunc func(in *wire.TickInput) (*wire.TickOutput, error)

TickFunc answers the hourly wake-up: what this app wants done, and when.

It runs with nobody waiting and nobody watching, so it is READ-ONLY — the same scope a screen gets. It may read settings and state (StateGet/StateList), look people up, notify, mail and make granted HTTP calls; it may NOT write files, write state, take locks or sign. The work itself happens in the actions it schedules, which are ordinary jobs.

Answer with nothing when there is nothing to do; that is the cheap case and the common one.

type UICallFunc added in v0.48.0

type UICallFunc func(in *wire.UICallInput) (any, error)

UICallFunc answers one call the app's OWN interface made (`fx.call(method, params)` in @brftech/filex-app-ui): any JSON-encodable value back, or an error that the interface's promise rejects with. It runs in screen mode — read inputs and settings, no file writes (docs/PLUGIN-KIT.md → Writing an interface).

type UIError added in v0.48.0

type UIError struct{ Text wire.Text }

UIError refuses an interface's call with a sentence per language; the interface's promise rejects with it (code `failed`), in the reader's language. A plain error works too, in whatever language it was written.

func (*UIError) Error added in v0.48.0

func (e *UIError) Error() string

type User

type User struct {
	ID    int64  `json:"user_id"`
	Email string `json:"email"`
	Name  string `json:"name,omitempty"`
}

User is a directory entry from UsersLookup.

func UsersLookup

func UsersLookup(q string) ([]User, error)

UsersLookup searches the caller's directory (permission `users:lookup`, tenant-scoped; at most 20 rows).

type ViewFunc

type ViewFunc func(in *wire.ViewEventInput) (*wire.Surface, error)

ViewFunc answers a view event with the next screen.

Directories

Path Synopsis
Package humandate writes a date the way filex's own screens write it, for an app that has no browser to ask.
Package humandate writes a date the way filex's own screens write it, for an app that has no browser to ask.
Package plugintest is the test kit that ships with the filex plugin SDK.
Package plugintest is the test kit that ships with the filex plugin SDK.
Package wire holds the JSON shapes that cross the host ↔ guest boundary of a filex app plugin.
Package wire holds the JSON shapes that cross the host ↔ guest boundary of a filex app plugin.

Jump to

Keyboard shortcuts

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