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
- Variables
- func IsRTL(tag string) bool
- func PersonalState(key string, userID int64) string
- func UILocaleKeyOK(k string) bool
- type Action
- type ActionRunInput
- type ActionRunOutput
- type Actor
- type Applies
- type CallContext
- type CoEditSpec
- type Condition
- type DescribeInput
- type EncryptedFolders
- type Field
- type FieldI18n
- type FieldOption
- type FileRef
- type HostError
- type JobRequest
- type Limits
- type Manifest
- type NewDocument
- type Node
- type OpenRequest
- type Output
- type OutputRef
- type PagePurpose
- type PublicPage
- type ScheduleItem
- type Section
- type Surface
- type SurfaceAction
- type Text
- type ThumbnailInput
- type ThumbnailOutput
- type ThumbnailSpec
- type TickInput
- type TickOutput
- type UIBundle
- type UICallInput
- type UICallOutput
- type UIExternal
- type UISpec
- type UserPermission
- type View
- type ViewEventInput
- type WasmSource
Constants ¶
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.
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.
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.
const ( ErrPermissionDenied = "permission_denied" ErrNotFound = "not_found" ErrTooLarge = "too_large" ErrTimeout = "timeout" 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.
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.
const ProtocolVersion = 1
ProtocolVersion is the manifest_version / ABI generation this package describes. A host refuses a plugin whose describe answer names another.
Variables ¶
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 ¶
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 ¶
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 ¶
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"`
// 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.
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"`
// 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 CoEditSpec ¶ added in v0.56.0
type CoEditSpec struct {
// Open: the app's `viewer` views open a file in its editing session -
// whoever opens the same file with the app edits it with them. Needs an
// interface (`ui`), a viewer view and `files:read`.
Open bool `json:"open,omitempty"`
}
CoEditSpec is the `co_edit` block (filex 0.56.0).
type DescribeInput ¶
type DescribeInput struct {
HostVersion string `json:"host_version"`
Locale string `json:"locale,omitempty"`
}
DescribeInput is what the host hands `describe`.
type EncryptedFolders ¶ added in v0.56.0
type EncryptedFolders struct {
// Open: the app's `viewer` views are offered on files in an unlocked
// encrypted folder, and their `file.read` / `file.save` carry the
// plaintext (FileInfo.plaintext). Needs an interface (`ui`), a viewer
// view and `files:read`. Not in a vault and not for a single encrypted
// file (`.fxe`) - filex 0.56 offers neither.
Open bool `json:"open,omitempty"`
}
EncryptedFolders is the `encrypted_folders` block (filex 0.56.0).
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 ¶
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 ¶
MarshalJSON writes the maps back when the field has them.
func (*Field) UnmarshalJSON ¶
UnmarshalJSON accepts label, help and placeholder as a string or as a {lang: …} map.
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 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"`
// EncryptedFolders says the app's interface edits files inside end-to-end
// encrypted folders (filex 0.56.0, task #189). The server holds such a file
// only as ciphertext and has no key to it, so filex never decrypts it on
// the server: the person's browser decrypts the file with the folder key
// it holds, hands the plaintext to the app's sandboxed frame only, and
// encrypts what the app saves before it leaves the browser. Derived
// permission `files:e2e-plaintext`, so the install review says it in its
// own words. filex before 0.56.0 does not know this field and refuses the
// manifest - declare it with `filex: ">=0.56.0"`.
EncryptedFolders *EncryptedFolders `json:"encrypted_folders,omitempty"`
// CoEdit says the app's interface edits a file together with other
// people (filex 0.56.0, task #189): its `viewer` views open a file in
// the file's editing session, through filex's relay (the bridge's
// `coedit.*`). Derived permission `files:co-edit`, so the install review
// says it in its own words. filex before 0.56.0 does not know this field
// and refuses the manifest - declare it with `filex: ">=0.56.0"`.
CoEdit *CoEditSpec `json:"co_edit,omitempty"`
}
Manifest is filex-app.json — and what `describe` must echo.
func (*Manifest) IsLanguagePack ¶
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 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 ¶
Text is a label in one or more languages. `en` is required everywhere a Text appears; other languages fall back to it.
func FillMessage ¶
FillMessage fills `{name}` placeholders in every language of t.
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
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"`
// FramePackage lets the interface open pages of its OWN package in
// frames of its own (an editor that puts the document in a frame, as
// ONLYOFFICE's DocsAPI does). Only this version's pages: frame-src and
// child-src name the package's own path and nothing else, and every
// such page is served under the same policy and sandbox - an opaque
// origin of its own, no storage, no connection the interface itself is
// not granted. The bridge stays with
// the frame filex drew; an inner frame talks to it with postMessage. A
// permission (`ui:frame-package`), derived like the rest of this block.
// filex 0.55 and later; an older filex refuses the field.
FramePackage bool `json:"frame_package,omitempty"`
// ConnectBlob lets the interface read `blob:` addresses it made itself
// with fetch/XHR (a document it unpacked in memory, loaded by an editor
// that only reads from an address): `blob:` in connect-src. A `blob:`
// address is the page's own memory and reaches no server. A permission
// (`ui:connect-blob`), derived like the rest of this block. filex 0.55
// and later; an older filex refuses the field.
ConnectBlob bool `json:"connect_blob,omitempty"`
// Print lets the interface hand filex a PDF to print (`ui.print`): a
// sandboxed frame may not open the browser's print dialog, so filex
// prints the PDF from its own page, on the person's gesture in the
// interface or their yes. A permission (`ui:print`), derived like the
// rest of this block - the same kind as Download, since the print
// dialog can save the PDF too. filex 0.55 and later; an older filex
// refuses the field.
Print bool `json:"print,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).