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, ui_call, thumbnail, 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
- Variables
- func EngineAvailable(name string) bool
- func FileLock(ref string, ttlDays int, reason string) (time.Time, error)
- func FileLockMessage(ref string, ttlDays int, key string, args map[string]string) (time.Time, error)
- func FileLockPath(qualified string, ttlDays int, reason string) (time.Time, error)
- func FileUnlock(ref string) error
- func FileUnlockPath(qualified string) error
- func HostSign(keyRef string, digest []byte) ([]byte, error)
- func IsNotFound(err error) bool
- func IsPermissionDenied(err error) bool
- func KeyDestroy(keyRef string) error
- func Log(level string, msg string)
- func MailSend(to, subject, body string) error
- func MailSendIn(lang, to, subject, body string) error
- func NotifySend(n Notice) (int64, error)
- func OpenFile(path, action, view string) *wire.OpenRequest
- func Progress(done, total int64, message string)
- func PublicPageRevoke(token string) errordeprecated
- func PublicPageState(token string, out any) errordeprecated
- func PublicPageStateSet(token string, state any) errordeprecated
- func ReadInput(ref string) ([]byte, error)
- func Run(p *Plugin)
- func Setting(key string) (string, bool, error)
- func SharePIN(token string) (pin string, reason string, err error)
- func ShareRevoke(token string) error
- func ShareState(token string, out any) error
- func ShareStateSet(token string, state any) error
- func StateDelete(ref, key string) error
- func StateGet(ref, key string) (string, bool, error)
- func StateSet(ref, key, value string) error
- func WriteOutput(name string, b []byte) (wire.OutputRef, error)
- type ActionFunc
- type Asset
- type EngineRequest
- type EngineResult
- type HTTPRequest
- type HTTPResponse
- type HostError
- type HostSigner
- type Input
- type IssuedCert
- type Notice
- type NoticeTarget
- type Output
- type PageCreate
- type PageCreated
- type PageFunc
- type Plugin
- type ShareFacts
- type SignInfo
- type StateItem
- type ThumbnailFunc
- type TickFunc
- type UICallFunc
- type UIError
- type User
- type ViewFunc
Constants ¶
const ChunkSize = 1 << 20
ChunkSize is the largest block one file_read/file_write moves.
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 ¶
var ErrNotRegistered = errors.New("pluginkit: no plugin registered (call pluginkit.Run in main)")
ErrNotRegistered is returned when an export is called before Run.
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 ¶
EngineAvailable reports whether the engine is installed AND granted.
func FileLock ¶
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 ¶
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 ¶
FileUnlock lifts this plugin's lock on an input (no-op when unlocked).
func FileUnlockPath ¶
FileUnlockPath is FileUnlock by adapter-qualified path.
func HostSign ¶
HostSign signs a sha256 digest with the key behind keyRef; the answer is a DER-encoded ECDSA signature (ecdsa-p256-sha256).
func IsNotFound ¶
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 ¶
IsPermissionDenied reports whether err is a refused host call.
func KeyDestroy ¶
KeyDestroy discards the private key behind keyRef; the certificate stays.
func Log ¶
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 ¶
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 ¶
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 ¶
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 ¶
Progress reports done/total (any unit; total 0 = unknown) and an optional message the tray shows.
func PublicPageRevoke
deprecated
func PublicPageState
deprecated
func PublicPageStateSet
deprecated
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 ¶
Setting reads one admin-configured setting (permission `settings`); secret fields are only readable here.
func SharePIN ¶
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 ShareState ¶
ShareState reads the link's durable record into out. Inside a page_event call token may be "" (the current link).
func ShareStateSet ¶
ShareStateSet replaces the link's durable record (≤ 64 KiB).
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 ¶
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.
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 ¶
OpenInput opens a file ref from ActionRunInput.Inputs (or a ref an engine produced) for reading.
func ThumbnailSource ¶ added in v0.50.0
func ThumbnailSource(in *wire.ThumbnailInput) (*Input, error)
ThumbnailSource opens the file a thumbnail call was handed, for reading. Close it when done.
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 ¶
CreateOutput opens a new output file (files:write).
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
// Thumbnail draws thumbnails of the kinds the manifest's `thumbnails`
// block names. filex refuses to install a manifest with that block whose
// module leaves this nil (the export answers an error then).
Thumbnail ThumbnailFunc
}
Plugin is what a program registers with Run.
type ShareFacts ¶
type ShareFacts struct {
// 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"`
// 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 bool `json:"has_pin"`
// 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 ¶
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 ¶
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 ThumbnailFunc ¶ added in v0.50.0
type ThumbnailFunc func(in *wire.ThumbnailInput) (*wire.ThumbnailOutput, error)
ThumbnailFunc draws one file's thumbnail (filex 0.50 and later): read the file with ThumbnailSource(in), answer a PNG or a JPEG (thumbkit.PNG). It is called only when the manifest has a `thumbnails` block naming the file's kind and the administrator left the app on for it (Admin → Plugins → Default apps).
⚠ The narrowest call there is. It is handed ONE file's bytes, its name, size and type - no path, no storage, no person - and every host function but reading that file, Setting, AssetFetch and HTTPDo (with an http: permission) answers permission_denied. Return an error when the file is not one you can draw: filex then asks the next handler in the administrator's list, or keeps the file's type icon.
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
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.
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 officecmd reads the command line an app hands the office engine.
|
Package officecmd reads the command line an app hands the office engine. |
|
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 thumbkit helps an app draw thumbnails (filex 0.50 and later): the answer's encoding, and a card of lines in the look filex's own archive and text thumbnails have - what an app that lists a file's contents (an archive kind filex does not read, a package, a playlist) wants to draw.
|
Package thumbkit helps an app draw thumbnails (filex 0.50 and later): the answer's encoding, and a card of lines in the look filex's own archive and text thumbnails have - what an app that lists a file's contents (an archive kind filex does not read, a package, a playlist) wants to draw. |
|
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. |