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

wire

package
v0.51.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package wire holds the JSON shapes that cross the host ↔ guest boundary of a filex app plugin. It is imported by the host (internal/wasmplugin) and by the guest SDK (pkg/pluginkit), so the two can never drift: a field added here is a field on both sides.

Everything here is plain data. No behaviour, no dependencies beyond the standard library, so it compiles unchanged for GOOS=wasip1.

Index

Constants

View Source
const (
	// MaxUILocaleBytes caps one language of one pack: the UTF-8 bytes of its
	// keys plus its values. 1 MiB is three times the largest complete
	// translation the catalogue allows today — room for the catalogue to
	// grow by half again in the wordiest two-byte script, and still a bound a
	// browser downloads in one breath.
	MaxUILocaleBytes = 1 << 20
	// MaxUILocalesBytes caps every language of one manifest together, so an
	// app may bundle several complete languages (a regional set, say)
	// without a manifest becoming a way to park arbitrary data on a server.
	MaxUILocalesBytes = 4 << 20
	// MaxUILocaleKeyBytes caps one key. The longest key in the catalogue is
	// 52 bytes (`userSettings.notifications.events.file_upload_failed`).
	MaxUILocaleKeyBytes = 128
	// MaxUILocaleValueBytes caps one translated string. The longest English
	// string is 606 bytes (`demo.alsoIncluded`); a language that runs 30 %
	// longer in a script of two-byte letters needs ~1.6 KB, so 4 KiB is room
	// to spare without admitting a paragraph that would wreck a layout.
	MaxUILocaleValueBytes = 4096
	// MaxManifestBytes caps the manifest DOCUMENT as it is uploaded or
	// fetched. The four mebibytes of strings above can arrive inflated: a
	// JSON writer that escapes non-ASCII (Python's json.dump does, by
	// default) writes each two-byte letter as a six-byte `\uXXXX` — three
	// times the bytes — so the document ceiling is the strings' ceiling times
	// three plus room for the rest of the manifest.
	MaxManifestBytes = 16 << 20
)

The catalogue these limits were sized against, re-measured on the shipped v0.43.0 tree: 3 593 keys — 1 555 in the explorer's catalogue (packages/core/src/locales/en.ts), 1 760 in the admin panel's (web/src/locales/en.json), 223 in the server's (the text filex writes into mail, notifications and the pages behind a link) and 55 drawn by both front-end catalogues — adding up to 194 961 bytes of UTF-8 for keys and values in English and 210 279 in Turkish. A script whose letters take two bytes (Arabic, Cyrillic, Greek, Hebrew) roughly doubles the value half, so a COMPLETE translation today is about 335 KB.

⚠ Re-measure it here when the catalogue moves: `node scripts/i18n-export.mjs` prints the key count, and the numbers above are what the ceilings below are justified by. They were still the 2026-09-21 figures (2 900 keys) after two waves of work had taken the catalogue to 3 593.

View Source
const (
	// ThumbnailMaxOutputBytes is the most an answer's image may weigh.
	ThumbnailMaxOutputBytes = 4 << 20
	// ThumbnailMaxPixels is the widest and the tallest an answer's image may
	// be, read from its header before a pixel is decoded.
	ThumbnailMaxPixels = 4096
	// ThumbnailSize is the box filex shows a thumbnail in, in pixels.
	ThumbnailSize = 320
)

The limits the host holds every thumbnail answer to - part of the contract, so the test kit checks an answer against the same numbers.

View Source
const (
	// ScheduleMaxItems is how many items one wake-up may schedule.
	ScheduleMaxItems = 64
	// ScheduleMaxPaths is how many files one item may name.
	ScheduleMaxPaths = 16
	// ScheduleKeyPattern is what an item key must match. It cannot match the
	// empty string, which is how the host keeps its own wake-up row out of
	// reach of the apps it wakes.
	ScheduleKeyPattern = `^[A-Za-z0-9][A-Za-z0-9_.:@-]{0,63}$`
)

The bounds a wake-up plans around. They are part of the CONTRACT, not the host's private business, because the guest is told them in every TickInput and the test kit checks an answer against them before a module is built — one set of numbers, named once.

View Source
const (
	ErrPermissionDenied = "permission_denied"
	ErrNotFound         = "not_found"
	ErrTooLarge         = "too_large"
	ErrTimeout          = "timeout"
	ErrUnavailable      = "unavailable"
	ErrInvalid          = "invalid"
	ErrBusy             = "busy"
	ErrInternal         = "internal"
	// ErrIntegrity: a downloaded asset did not match the sha256 the app
	// pinned (asset_fetch). Nothing was stored; nothing may use it.
	ErrIntegrity = "integrity"
)

Host function error codes.

View Source
const PersonalStateSuffix = "@me"

PersonalStateSuffix marks a PERSONAL state key: `<key>@<user id>` is kept on a file for one person, and filex shows it to that person — and to nobody else — as `<key>@me`. A rule names it that way (`state: ["todo@me"]`), so the signing app offers "Sign / Fill" only to the people who have something to sign on that file, while the key a listing exposes never says who else has. See PersonalState.

View Source
const ProtocolVersion = 1

ProtocolVersion is the manifest_version / ABI generation this package describes. A host refuses a plugin whose describe answer names another.

Variables

View Source
var SignerEKUs = [][]int{
	{1, 3, 6, 1, 5, 5, 7, 3, 36},
	{1, 2, 840, 113583, 1, 1, 5},
}

SignerEKUs are the extended key usages of every certificate the host issues for signing - a signer's (cert_issue) and the platform seal - as object identifiers. Both say "documents" and nothing else:

  • 1.3.6.1.5.5.7.3.36, id-kp-documentSigning (RFC 9336), the standard one; a verifier that asks for a document-signing certificate asks for it.
  • 1.2.840.113583.1.1.5, Adobe's Authentic Documents Trust: Acrobat refuses a signing certificate whose extendedKeyUsage carries none of emailProtection, codeSigning, anyExtendedKeyUsage or this one, and this is the only one of the four that is not a license for something else.

⚠ Never emailProtection (filex before 0.50.0 put it on every leaf): an app with `sign` chooses the name and the email address on the certificate, so emailProtection made it an S/MIME certificate in anybody's name under the tenant's authority - an organization-wide one once an operator imports their own. RFC 9336 exists to separate document signing from S/MIME and TLS for exactly this reason. Signatures made with the older certificates still verify: their chain is the same, and a verifier that requires document signing finds it on them too.

Functions

func IsRTL

func IsRTL(tag string) bool

IsRTL reports whether a language tag is written right to left. It is the ONE list: the `rtl` flag on the offered-language rows, the explorer's and the admin panel's layout direction, and the server-rendered pages' `dir` all come from it (docs/RTL.md).

func PersonalState

func PersonalState(key string, userID int64) string

PersonalState is the key an app writes for one person (`state_set`): PersonalState("todo", 7) is "todo@7", which user 7's listings show as "todo@me".

func UILocaleKeyOK

func UILocaleKeyOK(k string) bool

UILocaleKeyOK reports whether k may be a key of a language pack: dotted segments of letters, digits, `_` and `-` — the shape every key of both catalogues has — and none of the three segments that name a JavaScript object's machinery.

⚠⚠ The browser turns a dotted key into nested objects to feed the admin panel's vue-i18n (`a.b.c` → {a: {b: {c}}}). A segment called `__proto__` would walk that loop straight into Object.prototype and plant a property on every object of every visitor's page — public share pages included. The shape rule alone admits `__proto__` (underscores are legal), so the three are refused by name, here and again in the browser.

Types

type Action

type Action struct {
	ID      string  `json:"id"`
	Label   Text    `json:"label"`
	Icon    string  `json:"icon,omitempty"`
	Applies Applies `json:"applies"`
	View    string  `json:"view,omitempty"`
	Confirm Text    `json:"confirm,omitempty"`
	MinRole string  `json:"min_role,omitempty"` // viewer | editor | owner
	Danger  bool    `json:"danger,omitempty"`
	Output  Output  `json:"output"`
	Limits  Limits  `json:"limits,omitempty"`
	// Hidden keeps the action out of every menu: it is only ever started by
	// a surface (`job`) or a public page — the second half of a flow, not a
	// thing a person picks.
	Hidden bool `json:"hidden,omitempty"`
	// Requires names one of the manifest's `user_permissions` (its id): only
	// accounts that hold that permission see and run this action. Empty: the
	// action needs nothing beyond `min_role` and running apps at all.
	Requires string `json:"requires,omitempty"`
}

Action is one entry a plugin adds to the file menu.

type ActionRunInput

type ActionRunInput struct {
	JobID    string            `json:"job_id"`
	ActionID string            `json:"action_id"`
	Params   map[string]any    `json:"params,omitempty"`
	Inputs   []FileRef         `json:"inputs"`
	Output   Output            `json:"output"`
	Actor    Actor             `json:"actor"`
	Locale   string            `json:"locale,omitempty"`
	Settings map[string]string `json:"settings,omitempty"` // non-secret only
	Engines  map[string]bool   `json:"engines,omitempty"`
	// ShareMaxTTLDays: see CallContext.ShareMaxTTLDays. A job needs it for
	// the same reason a screen does — whatever it records about a link's
	// life (the request, the freeze beside it, the mail it sends) has to be
	// the life the link will actually get.
	ShareMaxTTLDays *int `json:"share_max_ttl_days,omitempty"`
}

ActionRunInput is what the host hands `action_run`.

type ActionRunOutput

type ActionRunOutput struct {
	OK      bool        `json:"ok"`
	Outputs []OutputRef `json:"outputs,omitempty"`
	Message Text        `json:"message,omitempty"`
	Surface *Surface    `json:"surface,omitempty"`
}

ActionRunOutput is what `action_run` returns.

type Actor

type Actor struct {
	ID    int64  `json:"id"`
	Email string `json:"email,omitempty"`
	Name  string `json:"name,omitempty"`
	Role  string `json:"role,omitempty"`
	// IP is the address the person's request came from, as filex's own
	// access log would write it (behind a trusted proxy, the forwarded
	// client). Set on a VIEW event — the screen somebody is looking at — and
	// empty on a job, which runs later and from nowhere in particular.
	//
	// ⚠ It exists for the same reason `data.page.visitor_ip` does on a
	// public page: an app that records WHO acted (the signing app's audit
	// trail, the IP line it can print under a signature) had that fact for
	// a stranger on a link and not for a signed-in person doing the same
	// thing — so the same box printed an address for one signer and "—" for
	// the next. Personal data: an app that records it must show it to the
	// person before it keeps it.
	IP string `json:"ip,omitempty"`
	// Permissions are the ids of THIS app's `user_permissions` the person
	// holds (filex 0.49.0 and later), decided exactly as filex decides them
	// at the door — an administrator holds every one. Set on a job
	// (`action_run`), a view event and an interface's call; absent on a
	// public page's event (nobody signed in) and on work nobody started.
	// Use Can.
	//
	// ⚠ Why an app needs it: filex refuses an action or a screen that
	// `requires` a permission before your code runs, but a hint on ANOTHER
	// screen ("…or Request signatures… to ask others") is drawn by you. The
	// signing app drew that hint for everybody, including a person the menu
	// no longer offered the action to. Say only what the reader can do.
	// It is a courtesy, not a gate — the door still decides.
	Permissions []string `json:"permissions,omitempty"`
}

Actor is the person a call runs as.

func (*Actor) Can added in v0.49.0

func (a *Actor) Can(id string) bool

Can reports whether the person holds the app's own user permission id (one of the manifest's `user_permissions`, `request` — not `app.sign.request`). False on a nil Actor, and on a filex older than 0.49.0, which does not say.

type Applies

type Applies struct {
	Kind  string   `json:"kind,omitempty"` // file | dir | any (default file)
	Ext   []string `json:"ext,omitempty"`  // lower-case, no dot
	Mime  []string `json:"mime,omitempty"` // exact or prefix glob "image/*"
	Multi bool     `json:"multi,omitempty"`
	Min   int      `json:"min,omitempty"`
	Max   int      `json:"max,omitempty"`
	// State narrows the offer to files on which THIS plugin keeps at least
	// one of the named state keys (`state_set`), NoState to files that
	// carry none of them. A signing app offers "Sign" only where a key
	// `pending` is set and "Request signatures" only where it is not. Keys
	// are the plugin's own; the listing exposes them as `<plugin>:<key>`.
	State   []string `json:"state,omitempty"`
	NoState []string `json:"no_state,omitempty"`
	// EngineExt adds extensions that are offered only while the named
	// engine is there and granted: `{"office": ["docx", "odt"]}` on top of
	// `ext: ["pdf"]` offers the action on a .docx exactly when the host can
	// turn one into a PDF (the office engine is the connected ONLYOFFICE;
	// `libreoffice`, its name before 0.50, reads the same). The host folds the list into Ext
	// before anybody sees the action (the explorer's menu, the run check),
	// so a client never evaluates it. It only ADDS to a non-empty ext/mime
	// list — an empty one already means "any file".
	//
	// ⚠⚠ Why (2026-09-21, a tester): "İmzala…" and "İmza iste…" were
	// offered on a .docx on an installation without LibreOffice, and the
	// click opened a page saying it is not a PDF and there is no LibreOffice
	// to convert it — while the Signatures screen itself said only PDFs can
	// be signed. An action that can never work must not be offered.
	EngineExt map[string][]string `json:"engine_ext,omitempty"`
	// Writable says the action can only be completed where its file can be
	// written, even though its own output is `none`: "Request signatures"
	// writes nothing now, but the flow it starts ends in writing the signed
	// document. filex hides such an action on a read-only storage (and
	// refuses it there with 409 read_only), exactly as it hides an action
	// whose output writes — instead of letting the app open a screen whose
	// only possible answer is "this cannot be done here". It needs the
	// files:write permission, and it counts as a write for the level the
	// caller needs (editor).
	Writable bool `json:"writable,omitempty"`
}

Applies says which selections an action or view is offered for.

type CallContext

type CallContext struct {
	Inputs   []FileRef         `json:"inputs,omitempty"`
	Actor    *Actor            `json:"actor,omitempty"`
	Locale   string            `json:"locale,omitempty"`
	Settings map[string]string `json:"settings,omitempty"`
	Engines  map[string]bool   `json:"engines,omitempty"`
	// ShareMaxTTLDays is the longest life, in days, this installation gives
	// ANY new share link — the administrator's ceiling (Protection → share
	// links, `share.max_ttl_days`; 7 unless it was changed). 0 means no
	// ceiling; nil means the host did not say (older than v0.43.0) or this
	// app holds no `public_pages` grant and so opens no links.
	//
	// ⚠⚠ share_create clamps every link to the LOWEST of this, the page's own
	// `max_ttl_days` and 365. A screen that offers a longer life than that
	// promises something the link will not have: the signing app's wizard
	// said "links valid 14 days" while the link it opened lived 7, because
	// the ceiling was invisible from inside the app. Read it, and offer no
	// more than it allows.
	ShareMaxTTLDays *int `json:"share_max_ttl_days,omitempty"`
	// Home is where this person's results go by default when they cannot
	// go beside their source (Output.Elsewhere): the root of the first
	// storage, in the administrator's order, that the person may write.
	// Adapter-qualified (`depo://`); empty when they may write nowhere.
	Home string `json:"home,omitempty"`
}

CallContext is the shared context of a view or page call.

type Condition

type Condition struct {
	Key    string   `json:"key"`
	Equals []string `json:"equals"`
}

Condition is "that other field holds one of these values".

type DescribeInput

type DescribeInput struct {
	HostVersion string `json:"host_version"`
	Locale      string `json:"locale,omitempty"`
}

DescribeInput is what the host hands `describe`.

type Field

type Field struct {
	Key         string        `json:"key"`
	Type        string        `json:"type"` // string | password | int | bool | select | text
	Label       string        `json:"label"`
	Help        string        `json:"help,omitempty"`
	Required    bool          `json:"required,omitempty"`
	Secret      bool          `json:"secret,omitempty"`
	Default     any           `json:"default,omitempty"`
	Placeholder string        `json:"placeholder,omitempty"`
	Options     []FieldOption `json:"options,omitempty"`
	Min         *int          `json:"min,omitempty"`
	Max         *int          `json:"max,omitempty"`
	// Multi turns a `select` into a several-of choice. The value is then a
	// list of option values instead of one.
	Multi bool `json:"multi,omitempty"`
	// Style says how a `bool` is drawn: `switch` for an on/off setting a
	// person flips in passing, `choice` for a decision they must read
	// before answering (two buttons, Yes and No). Empty lets the client
	// decide, which it does by guessing — say it when the answer matters.
	Style string `json:"style,omitempty"`
	// ShowWhen hides the field until another field in the same form holds
	// one of the given values; RequiredWhen makes it required only then.
	// Both are re-checked by the host at submit: a value belonging to a
	// hidden field is dropped, and a required-when field that is empty
	// refuses the job. A form may not present a contradiction (a file name
	// asked for while the output is a new version of the same file).
	ShowWhen     *Condition `json:"show_when,omitempty"`
	RequiredWhen *Condition `json:"required_when,omitempty"`
	// I18n is the per-language spelling of Label, Help and Placeholder, when
	// they were written as {"en": …, "tr": …} instead of one string. Label,
	// Help and Placeholder then hold the English (or the only) spelling, so
	// code that reads them as strings keeps working, and the field is
	// written back out as the maps it arrived as.
	//
	// ⚠⚠ Why (2026-09-21, a tester): an app's settings were English inside
	// the Turkish admin panel — "Add a time stamp to every signature",
	// "Time-stamping authority" — because a manifest setting could only be
	// ONE string, while every other text an app shows (its name, its
	// actions, its screens) is a {en, tr} map the client already resolves.
	I18n *FieldI18n `json:"-"`
}

Field is one input of a settings or action form. It mirrors the storage driver descriptor field the admin panel already renders (storage.Field), so a plugin's settings form is drawn by the same component.

func (Field) Localized

func (f Field) Localized(lang string) Field

Localized is the field with every text in `lang` (falling back to English) and no map left — for a client that reads plain strings.

func (Field) MarshalJSON

func (f Field) MarshalJSON() ([]byte, error)

MarshalJSON writes the maps back when the field has them.

func (*Field) UnmarshalJSON

func (f *Field) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts label, help and placeholder as a string or as a {lang: …} map.

type FieldI18n

type FieldI18n struct {
	Label       Text
	Help        Text
	Placeholder Text
}

FieldI18n holds a Field's texts in every language the manifest gave.

type FieldOption

type FieldOption struct {
	Value     string `json:"value"`
	Label     string `json:"label"`
	LabelI18n Text   `json:"-"`
}

FieldOption is one choice of a select field. Its label, like a Field's, may be one string or a {lang: …} map (then kept in LabelI18n).

func (FieldOption) Localized

func (o FieldOption) Localized(lang string) FieldOption

Localized is the option with its label in `lang`.

func (FieldOption) MarshalJSON

func (o FieldOption) MarshalJSON() ([]byte, error)

MarshalJSON writes the map back when the option has one.

func (*FieldOption) UnmarshalJSON

func (o *FieldOption) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts the label as a string or a {lang: …} map.

type FileRef

type FileRef struct {
	Ref     string `json:"ref"`
	Name    string `json:"name"`
	Size    int64  `json:"size"`
	Mime    string `json:"mime,omitempty"`
	PathRel string `json:"path_rel,omitempty"`
	// Path is the adapter-qualified form (`docs://reports/nda.pdf`) — what
	// the explorer navigates by and what a `pdf-fields`/`preview` node's
	// `src.path` takes. Filled by the host whenever the storage is known.
	Path string `json:"path,omitempty"`
	// ReadOnly says the file's storage takes no writes: no new version, no
	// file beside it. Filled by the host whenever the storage is known.
	//
	// ⚠⚠ Why an app needs to know BEFORE it writes (2026-09-21, a tester):
	// a signing request on a read-only storage was accepted and "sent" —
	// the file frozen, the signer notified — and the signer's answer then
	// failed with 409 read_only, for ever, because the request writes
	// nothing itself (its output mode is `none`) and the host's own
	// read-only refusal only guards jobs that write. Only the app knows
	// that the flow it starts ends in a write, so the app has to be told,
	// and has to refuse at the start with a sentence.
	ReadOnly bool `json:"read_only,omitempty"`
}

FileRef is one file the call may read through file_* host functions. Ref is an opaque, call-scoped handle — never a storage path.

type HostError

type HostError struct {
	Code    string `json:"code"`
	Message string `json:"message,omitempty"`
}

HostError is the error envelope every host function may return.

type JobRequest

type JobRequest struct {
	ActionID string         `json:"action_id"`
	Params   map[string]any `json:"params,omitempty"`
	// Output, when set, replaces the action's manifest output for this one
	// job — a wizard's "same file as a new version / new file beside it /
	// custom name" choice. Mode is sibling | version | none; Name takes the
	// same {stem}/{ext}/{name} pattern as the manifest (a literal name works
	// too). Widening `none` to a writing mode is allowed only if the caller
	// holds editor on the inputs — the host re-checks at submit.
	Output *Output `json:"output,omitempty"`
}

JobRequest asks the host to enqueue an action with these params.

type Limits

type Limits struct {
	MemoryPages   int `json:"memory_pages,omitempty"`
	CallTimeoutS  int `json:"call_timeout_s,omitempty"`
	MaxInputBytes int `json:"max_input_bytes,omitempty"`
	TimeoutS      int `json:"timeout_s,omitempty"`
}

Limits are what a plugin asks for; the host clamps them.

type Manifest

type Manifest struct {
	ManifestVersion int    `json:"manifest_version"`
	Name            string `json:"name"`
	Version         string `json:"version"`
	Label           Text   `json:"label"`
	Description     Text   `json:"description,omitempty"`
	Icon            string `json:"icon,omitempty"`
	Homepage        string `json:"homepage,omitempty"`
	// Filex is the range of filex versions the app works with, e.g.
	// ">=0.47.0" or ">=0.47.0 <0.60.0" (comparators joined by spaces must
	// all hold; alternatives are joined by "||"). Empty = any filex. filex
	// refuses to install or upgrade to a version whose range leaves the
	// running filex out, and its update check picks the newest version whose
	// range lets it in (docs/PLUGIN-KIT.md → Which filex it works with).
	// ⚠ filex before 0.47.0 does not know this field and refuses a manifest
	// that carries it; an app that must still install there says only
	// `min_filex`.
	Filex string `json:"filex,omitempty"`
	// MinFilex is the older, lower-bound-only form: "0.43.0" means
	// ">=0.43.0". Honoured together with Filex (both must hold).
	MinFilex    string   `json:"min_filex,omitempty"`
	Permissions []string `json:"permissions"`
	// Languages the plugin promises to speak. Every Text it returns must
	// carry all of them, or the host refuses it at install: a screen half in
	// one language is a bug the author should see before a person does.
	// Empty means ["en"].
	Languages []string `json:"languages,omitempty"`
	// UILocales lets a plugin add a language to FILEX ITSELF — the key is a
	// language tag, the value that language's strings by filex's own keys.
	// They join the language list the interface offers (public pages
	// included) and leave with the plugin. A missing key falls back to
	// English, as everywhere else.
	UILocales         map[string]map[string]string `json:"ui_locales,omitempty"`
	PermissionReasons map[string]Text              `json:"permission_reasons,omitempty"`
	Settings          []Field                      `json:"settings,omitempty"`
	Actions           []Action                     `json:"actions,omitempty"`
	// UserPermissions are what the app lets the administrator grant per role
	// and per person (app.<app>.<id>); actions and views name one in
	// `requires`. filex before 0.49.0 does not know this field and refuses
	// the manifest — declare it with `filex: ">=0.49.0"`.
	UserPermissions []UserPermission `json:"user_permissions,omitempty"`
	Views           []View           `json:"views,omitempty"`
	PublicPages     []PublicPage     `json:"public_pages,omitempty"`
	Limits          Limits           `json:"limits,omitempty"`
	Wasm            *WasmSource      `json:"wasm,omitempty"`
	// UI is the app's own interface (HTML/JS/CSS in a zip), served by filex
	// in a sandboxed frame. An app may have a module, an interface, or both.
	UI *UISpec `json:"ui,omitempty"`
	// NewDocuments are rows the app adds to filex's "New" menu: a file of
	// that kind, empty or made from a template in the interface's package,
	// opened in one of the app's `viewer` views. Each kind is a permission
	// (`ui-new:.<ext>`), derived like the interface's own.
	NewDocuments []NewDocument `json:"new_documents,omitempty"`
	// Messages are texts FILEX says on the app's behalf, long after the call
	// that caused them — today the reason beside a file lock ("signatures
	// are being collected"), shown in the admin panel, the details panel
	// and a refused rename, each in its READER's language. The app names
	// one by key when it locks (pluginkit.FileLockMessage); `{placeholders}`
	// are filled from the arguments it passes. Every declared language is
	// required, like every other Text.
	Messages map[string]Text `json:"messages,omitempty"`
	// Thumbnails says the app draws thumbnails of the kinds its Applies names
	// (filex 0.50.0 and later): its module answers the `thumbnail` export. Each
	// kind is a permission (`thumbnail:.<ext>` / `thumbnail:<type>`), derived
	// like the interface's, so an upgrade that draws one more kind is a new
	// grant. filex before 0.50.0 does not know this field and refuses the
	// manifest - declare it with `filex: ">=0.50.0"`.
	Thumbnails *ThumbnailSpec `json:"thumbnails,omitempty"`
}

Manifest is filex-app.json — and what `describe` must echo.

func (*Manifest) IsLanguagePack

func (m *Manifest) IsLanguagePack() bool

IsLanguagePack reports whether the manifest is a language pack and nothing else: it adds at least one language to filex and declares nothing a module could run — no action, no screen, no public page, no setting, no permission, no module address and no interface of its own.

⚠⚠ Such an app has NO module. It installs from its manifest alone, never starts a runtime instance, and is listed as a language pack. A translator must not need a Go toolchain to ship a translation, and a module that could do nothing would still be a binary the administrator is asked to trust.

⚠ `wasm` present means "there is a module" and keeps the manifest an ordinary app, so an app that was built the old way (a module whose only job was to carry `ui_locales`) keeps loading exactly as it did.

type NewDocument added in v0.48.0

type NewDocument struct {
	Ext      string `json:"ext"`
	Label    Text   `json:"label"`
	View     string `json:"view"`
	Template string `json:"template,omitempty"`
}

NewDocument is one row of the "New" menu an app adds. Ext is the kind (lower-case, no dot); View is the app's `viewer` view that opens it — it must open that kind; Template, when given, is a file of the interface's package the new file is a copy of (else the file is empty).

type Node

type Node struct {
	ID       string         `json:"id,omitempty"`
	Type     string         `json:"type"`
	Props    map[string]any `json:"props,omitempty"`
	Children []Node         `json:"children,omitempty"`
}

Node is one component. Type is from the host's catalogue; Props are that component's properties; Children only for layout nodes.

type OpenRequest

type OpenRequest struct {
	Path   string `json:"path"`
	Action string `json:"action,omitempty"`
	View   string `json:"view,omitempty"`
}

OpenRequest is "go to this file, and start this there". Path is adapter-qualified (`docs://reports/nda.pdf`). Action and View are this plugin's own; naming neither just opens the file.

type Output

type Output struct {
	Mode string `json:"mode"`           // sibling | version | none | folder (per job)
	Name string `json:"name,omitempty"` // pattern: {stem}, {ext}, {name}
	// Elsewhere (manifest, with mode sibling) says the result may go into a
	// folder the PERSON chooses instead of beside its source: where the
	// source's storage is read-only, filex still offers the action and the
	// app asks where the result should go (a `file-chooser` with kind "dir",
	// starting at CallContext.Home), then answers the job with Output{Mode:
	// "folder", Dir: <the chosen folder>}. filex checks that folder — the
	// storage, the person's level on it, locks and filex's own folders —
	// before the job is queued and again when the file is written.
	Elsewhere bool `json:"elsewhere,omitempty"`
	// Dir is the chosen folder for mode "folder", adapter-qualified
	// (`docs://reports`). Only a JobRequest carries it.
	Dir string `json:"dir,omitempty"`
}

Output says where an action's result files land.

type OutputRef

type OutputRef struct {
	Ref  string `json:"ref"`
	Name string `json:"name"`
}

OutputRef names a file the plugin produced through file_create.

type PagePurpose

type PagePurpose struct {
	// Label names the kind of link in a list: "Signing request".
	Label Text `json:"label"`
	// Revoke is said before the link is revoked, plainly: "Revoking this
	// link cancels the signing request."
	Revoke Text `json:"revoke,omitempty"`
	// Section is the section of the app's `home` view the row opens
	// (Surface.Sections); empty opens the home view as it comes.
	Section string `json:"section,omitempty"`
}

PagePurpose names what an app's link is part of.

⚠⚠ Why (2026-09-21, the owner's decision): signing links appeared in My shares as plain `/teklif.pdf` shares offering "Copy PIN" and "Revoke", with no hint they belonged to a signing request — and revoking one broke the request. They stay listed, but marked: named for what they are, opening the app's page for them, and saying before a revoke what the revoke does.

type PublicPage

type PublicPage struct {
	ID             string `json:"id"`
	Label          Text   `json:"label"`
	PIN            string `json:"pin,omitempty"` // optional | required | none
	DefaultTTLDays int    `json:"default_ttl_days,omitempty"`
	MaxTTLDays     int    `json:"max_ttl_days,omitempty"`
	// Purpose says what a link of this page IS, for the person who sees it
	// in a list of links (My shares, the admin's Shares): it belongs to
	// something the app keeps, ending it ends that, and there is a place in
	// the app that shows it. Optional; without it a link lists as a share.
	Purpose *PagePurpose `json:"purpose,omitempty"`
}

PublicPage is a flow an outside participant reaches through a share link, /s/<token> (the retired /p/<token> answers a 301 to it).

type ScheduleItem

type ScheduleItem struct {
	// Key is the app's own name for this piece of work, and the reason a
	// restart cannot run it twice: the host keeps at most one item per
	// (app, key), so naming the same key again moves the item rather than
	// adding a second one. A signing app keys by envelope
	// (`expire:7f3a…`). 1–64 characters of [A-Za-z0-9] plus `_.:@-`.
	Key string `json:"key"`
	// DueAt is when the work runs, to the minute (to the second, in fact).
	// A time already past runs at once; a time beyond WindowEnd is not
	// scheduled.
	DueAt time.Time `json:"due_at"`
	// ActionID is the app's action to run. It must exist in the manifest
	// and not be switched off by the administrator.
	ActionID string `json:"action_id"`
	// Paths are the files the job runs on, adapter-qualified
	// (`docs://reports/nda.pdf`) — the spelling `state_list` answers with.
	// At least one, at most MaxPaths, all on the same storage.
	Paths  []string       `json:"paths"`
	Params map[string]any `json:"params,omitempty"`
}

ScheduleItem is one piece of work an app wants run at a named minute.

It is an action of the app's own — usually a `hidden` one, the half of a flow nobody picks from a menu — run as an ordinary plugin job: the same ops row, the same sandbox, the same output rules. The wake-up decides WHAT and WHEN; the job does the work.

type Section

type Section struct {
	ID    string `json:"id"`
	Label Text   `json:"label"`
	// Count is drawn beside the label (a number of rows waiting there);
	// nil draws none.
	Count *int `json:"count,omitempty"`
}

Section is one entry of a home page's menu.

type Surface

type Surface struct {
	Title   Text            `json:"title,omitempty"`
	Size    string          `json:"size,omitempty"`
	State   map[string]any  `json:"state,omitempty"`
	Nodes   []Node          `json:"nodes"`
	Actions []SurfaceAction `json:"actions,omitempty"`
	Toast   Text            `json:"toast,omitempty"`
	Done    bool            `json:"done,omitempty"`
	Job     *JobRequest     `json:"job,omitempty"`
	Errors  map[string]Text `json:"errors,omitempty"`
	// Open sends the person to a FILE — the one thing a screen could not do
	// before. A home screen lists documents; a row has to be able to open
	// the one that was clicked, and open it on the right screen. The host
	// checks the path and the target before the answer leaves.
	Open *OpenRequest `json:"open,omitempty"`

	// Sections turn a `home` screen into a page with a MENU: one entry per
	// section, the frame drawing the menu and the plugin drawing whichever
	// section is open. Section says which one this surface is.
	//
	// ⚠⚠ Why the frame and not a node. The owner, 2026-09-21: the
	// Signatures screen should be "its own page … each in a separate menu",
	// with the browser's Back button working and a notification landing on
	// the right section. A node can draw tabs, but only the FRAME owns the
	// address bar: it keeps the open section in the URL (`?section=`), so
	// Back walks the sections and a link can name one. Opening a view with a
	// section in its address hands the plugin `data.section` on the `open`
	// event; everything after that is an ordinary conversation.
	Sections []Section `json:"sections,omitempty"`
	Section  string    `json:"section,omitempty"`
}

Surface is a declarative screen: nodes drawn by filex's own components.

type SurfaceAction

type SurfaceAction struct {
	ID       string `json:"id"`
	Label    Text   `json:"label"`
	Primary  bool   `json:"primary,omitempty"`
	Danger   bool   `json:"danger,omitempty"`
	Disabled bool   `json:"disabled,omitempty"`
}

SurfaceAction is a footer button.

type Text

type Text map[string]string

Text is a label in one or more languages. `en` is required everywhere a Text appears; other languages fall back to it.

func FillMessage

func FillMessage(t Text, args map[string]string) Text

FillMessage fills `{name}` placeholders in every language of t.

func (Text) Get

func (t Text) Get(lang string) string

Get returns the text for lang, falling back to its base language (`pt` for `pt-br`), then to English, then to any — per string (textGet, langpack.go).

type ThumbnailInput added in v0.50.0

type ThumbnailInput struct {
	File FileRef `json:"file"`
	// Ext is the file name's extension, lower-case, no dot ("" for none).
	Ext string `json:"ext,omitempty"`
	// MaxWidth / MaxHeight are the box filex shows a thumbnail in. Drawing
	// larger is wasted: filex scales the answer down to fit.
	MaxWidth  int `json:"max_width"`
	MaxHeight int `json:"max_height"`
	// MaxOutputBytes is the most the answer's image may weigh.
	MaxOutputBytes int `json:"max_output_bytes"`
	// Locale is the instance's default language (a thumbnail is the same
	// picture for everybody).
	Locale   string            `json:"locale,omitempty"`
	Settings map[string]string `json:"settings,omitempty"` // non-secret only
}

ThumbnailInput is what the host hands `thumbnail`: ONE file to draw.

The file is the call's only input (`File.Ref`, read with file_open): its name, size and type, never its path, its storage or who owns it. Nobody is signed in to a thumbnail call; it runs whenever filex draws the file (an upload, a listing that found it stale, a repair), for every person alike.

type ThumbnailOutput added in v0.50.0

type ThumbnailOutput struct {
	Image []byte `json:"image"`
}

ThumbnailOutput is what `thumbnail` answers: a PNG or a JPEG. filex scales it, lays it on the transparency checkerboard and writes its own JPEG. An app that cannot draw this file answers an error; filex then asks the next handler in the administrator's list.

type ThumbnailSpec added in v0.50.0

type ThumbnailSpec struct {
	Applies Applies `json:"applies"`
}

ThumbnailSpec is the `thumbnails` block: the kinds of file the app draws. Applies names them the way a viewer's rule does - a non-empty ext or mime list (a family like `image/*` is fine, `*/*` is not), kind `file`.

type TickInput

type TickInput struct {
	// Now is the host's clock when the wake-up fired, in UTC.
	Now time.Time `json:"now"`
	// WindowStart and WindowEnd bound the due times this answer may name.
	// WindowStart equals Now; WindowEnd is the next hourly boundary, which
	// is when the next wake-up comes. An item due outside the window is not
	// scheduled and no error is raised — say it again at the wake-up whose
	// window contains it. After a restart at 03:37 the window is 03:37 to
	// 04:00, so it is shorter than an hour and the app is told so.
	WindowStart time.Time `json:"window_start"`
	WindowEnd   time.Time `json:"window_end"`
	// MaxItems is how many items this answer may carry. Extra ones are
	// dropped (the app's log says how many), so put the urgent ones first.
	MaxItems int `json:"max_items"`
	// MaxPaths is how many files one item may name.
	MaxPaths int               `json:"max_paths"`
	Locale   string            `json:"locale,omitempty"`
	Settings map[string]string `json:"settings,omitempty"` // non-secret only
	Engines  map[string]bool   `json:"engines,omitempty"`
}

TickInput is what the host hands `tick`.

type TickOutput

type TickOutput struct {
	Items []ScheduleItem `json:"items,omitempty"`
	// Note is a line for the app's log in the admin panel — what this
	// wake-up decided, in the app's own words. It is not shown to anyone
	// else; there is nobody there.
	Note Text `json:"note,omitempty"`
}

TickOutput is what `tick` returns.

type UIBundle added in v0.48.0

type UIBundle struct {
	URL    string `json:"url,omitempty"`
	SHA256 string `json:"sha256,omitempty"`
}

UIBundle says where the interface's zip is and what it hashes to. URL takes `{tag}` like WasmSource.URL; an upload carries the zip itself.

type UICallInput added in v0.48.0

type UICallInput struct {
	ViewID  string          `json:"view_id"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params,omitempty"`
	Context CallContext     `json:"context"`
}

UICallInput is what the host hands `ui_call`: one call an app's OWN interface made to its module over the bridge (`engine.call`, docs/APP-PLUGINS-API.md → An app's own interface). Method and Params are the interface's; Context is the same a screen gets — the files the interface was opened with as refs, the person, the language, the settings.

It runs in SCREEN mode: it may read its inputs and settings, look people up, keep state; it may not write files, take locks or run engines — a write is the interface's own save, or a job (`job.submit`).

type UICallOutput added in v0.48.0

type UICallOutput struct {
	Result any  `json:"result,omitempty"`
	Error  Text `json:"error,omitempty"`
}

UICallOutput is what `ui_call` answers: any JSON value for the interface. Error, when set, refuses the call with a sentence in every language the app speaks (the interface's promise rejects with code `failed` and the text in the reader's language).

type UIExternal added in v0.48.0

type UIExternal struct {
	URL string `json:"url"`
	// As is what the file is: style | font | img | media. Never script, never
	// connect.
	As     string `json:"as"`
	SHA256 string `json:"sha256,omitempty"`
	Reason Text   `json:"reason,omitempty"`
}

UIExternal is one address outside the package.

With SHA256 the file is MIRRORED: filex downloads it once, at install, checks the hash and serves it from the package's own address as `ext/<host>/<path>`, so no browser asks the address. Without it the address is LIVE — the reader's browser fetches it — and that is a permission (`ui-net:<as>:<url>`) the administrator approves.

type UISpec added in v0.48.0

type UISpec struct {
	// Bundle is the zip of the interface's files.
	Bundle UIBundle `json:"bundle"`
	// CSP names the exceptions to the interface's script policy:
	// "unsafe-eval" and "wasm-unsafe-eval". Each is a permission.
	CSP []string `json:"csp,omitempty"`
	// External are the addresses outside the package the interface loads.
	External []UIExternal `json:"external,omitempty"`
	// PackageFetch lets the interface READ its own package with fetch/XHR
	// (an editor that loads its stencils or translations at run time, as
	// draw.io does). Only this version's files: connect-src and the
	// Connection-Allowlist name the package's own path and nothing else. A
	// permission (`ui:package-fetch`), derived like the rest of this block.
	PackageFetch bool `json:"package_fetch,omitempty"`
	// Download lets the interface hand the person a file to keep on their
	// own disk (`ui.download` in the SDK): through filex, each time on the
	// person's gesture or their yes, never over the limit. A permission
	// (`ui:download`), derived like the rest of this block.
	Download bool `json:"download,omitempty"`
}

UISpec is the `ui` block: the app's own interface.

type UserPermission added in v0.49.0

type UserPermission struct {
	ID          string `json:"id"`
	Label       Text   `json:"label"`
	Description Text   `json:"description,omitempty"`
	// Default is who holds it until the administrator decides: `viewer`
	// (every account), `user` (accounts that can change files — the usual
	// choice) or `admin` (administrators only, until granted).
	Default string `json:"default"`
}

UserPermission is a permission the app lets the administrator hand out per role and per person ("Request signatures"): filex lists it in the role editor under the app's name, as app.<app>.<id>, and an action or a view that `requires` it is refused to an account that does not hold it. Signing what somebody sent you needs no such permission; asking people to sign does — an app declares only what an organisation would want to limit.

type View

type View struct {
	ID        string  `json:"id"`
	Placement string  `json:"placement"` // modal | page | inspector | home | viewer
	Label     Text    `json:"label"`
	Applies   Applies `json:"applies,omitempty"`
	Size      string  `json:"size,omitempty"`
	// UI is the bundle file this view opens ("index.html", "editor/main.html"):
	// the view is the app's own interface, served by filex in a sandboxed
	// frame (docs/APP-PLUGINS-API.md → An app's own interface). Empty: the
	// view is a Surface the module draws.
	UI string `json:"ui,omitempty"`
	// Requires: as Action.Requires — the view (and every job it starts) is
	// refused to an account without that user permission.
	Requires string `json:"requires,omitempty"`
}

View is a screen the plugin can be asked to draw — with filex's own components (a Surface its module answers), or with the app's own interface (UI names a file of the `ui` bundle).

Placement: `modal` opens over the explorer; `page` opens as a full page in a new tab (a wizard with a document beside it — anything a modal's double scroll would cramp); `inspector` is a section of the details panel; `home` is a standalone screen listed under Apps; `viewer` (an interface only) opens a file the way filex's own viewers do, in the preview's place.

type ViewEventInput

type ViewEventInput struct {
	ViewID   string         `json:"view_id"`
	Event    string         `json:"event"` // open | change | submit | action
	ActionID string         `json:"action_id,omitempty"`
	State    map[string]any `json:"state,omitempty"`
	Data     map[string]any `json:"data,omitempty"`
	Context  CallContext    `json:"context"`
}

ViewEventInput is what the host hands `view_event`.

type WasmSource

type WasmSource struct {
	URL    string `json:"url,omitempty"`
	SHA256 string `json:"sha256,omitempty"`
}

WasmSource says where the module comes from when installing from a manifest fetched off a repository (GitHub URL install).

Jump to

Keyboard shortcuts

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