@brftech/filex-app-ui 0.56.3 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/filex-app-ui.iife.js +1 -1
- package/dist/filex-app-ui.iife.js.map +1 -1
- package/dist/filex-app-ui.js +43 -42
- package/dist/filex-app-ui.js.map +1 -1
- package/dist/index.d.ts +117 -2
- package/package.json +1 -1
- package/src/index.ts +22 -2
- package/src/protocol.ts +101 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install @brftech/filex-app-ui
|
|
|
19
19
|
import { connect } from '@brftech/filex-app-ui';
|
|
20
20
|
|
|
21
21
|
const fx = await connect(); // the handshake; rejects outside filex
|
|
22
|
-
const file = await fx.open(); // { name, ext, size, mime, readOnly, text(), bytes(), stream(), save() }
|
|
22
|
+
const file = await fx.open(); // { name, ext, size, mime, readOnly, text(), bytes(), stream(), read(), save() }
|
|
23
23
|
editor.value = await file.text();
|
|
24
24
|
editor.addEventListener('input', () => fx.dirty(true));
|
|
25
25
|
fx.onSave(() => editor.value); // filex's Save, a draft's "Save to disk", Ctrl+S
|
|
@@ -31,7 +31,7 @@ with a `<script src>`: it defines `window.FilexAppUI.connect`.
|
|
|
31
31
|
| Call | What it does |
|
|
32
32
|
|---|---|
|
|
33
33
|
| `fx.session` | the app and view, the reader's language and direction, filex's colours, the person's display name, the files (each with `encrypted: 'folder' \| 'vault' \| 'file'` when it is end-to-end encrypted, filex 0.55 - on its own information only: the file is `readOnly` and its read is refused; `plaintext: true` beside `encrypted: 'folder'` or `'vault'`, filex 0.56: the app asked for `encrypted_folders` and filex hands the file over in the clear - decrypted in the person's browser for `open()`, encrypted again on `save()`, at most `LIMITS.maxEncryptedBytes`; `saveAs` and the module are not offered while such a file is open), the grants |
|
|
34
|
-
| `fx.open(i)` / `fx.save(data)` / `fx.saveAs(name, data)` | read an opened file; save over it (a new version, or the draft it is); save a new file where the person picks |
|
|
34
|
+
| `fx.open(i)` / `fx.save(data)` / `fx.saveAs(name, data)` | read an opened file; save over it (a new version, or the draft it is); save a new file where the person picks. A `.csv` (filex 0.57): `file.read(as)` answers `text_dialect` - its delimiter, byte order mark, line ends and encoding - and `text()` is decoded in that encoding; `fx.save(data, {csv: {delimiter, encoding, bom, eol}})` says how the saved CSV is written, which filex checks against the bytes before it writes the file in its own way ([APP-PLUGINS-API.md → CSV files](https://github.com/BRF-Tech/filex/blob/main/docs/APP-PLUGINS-API.md#csv-files-how-they-are-written)) |
|
|
35
35
|
| `fx.dirty(on)`, `fx.title(t)`, `fx.toast(t, tone)`, `fx.confirm(opts)`, `fx.close()` | tell filex about unsaved changes; ask filex to draw a line, a question, to close (a toast and a question carry your app's name) |
|
|
36
36
|
| `fx.copy(text)` | put text on the clipboard - call it from a click or a key press: without one, filex asks the person first |
|
|
37
37
|
| `fx.download(name, data, mime?)` | hand the person a file for their own disk (`"ui": { "download": true }` in the manifest) - from a click, or filex asks; 256 MiB at most; Chromium streams it into the file they pick |
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var FilexAppUI=function(o){"use strict";const S="filex:hello",B="filex:port",O=["session.get","file.read","file.save","file.saveAs","ui.dirty","ui.title","ui.toast","ui.confirm","ui.close","clipboard.write","ui.download","engine.call","job.submit","state.get","state.set","license.get","ui.print","coedit.join","coedit.subscribe","coedit.append","coedit.lease","coedit.cursor","coedit.blob.put","coedit.blob.get","coedit.leave"],h={maxTextBytes:33554432,maxStateBytes:8192,maxLineChars:300,connectTimeoutMs:1e4,maxDownloadBytes:268435456,maxPrintBytes:67108864,maxEncryptedBytes:209715200};function
|
|
1
|
+
var FilexAppUI=function(o){"use strict";const S="filex:hello",B="filex:port",O=["session.get","file.read","file.save","file.saveAs","ui.dirty","ui.title","ui.toast","ui.confirm","ui.close","clipboard.write","ui.download","engine.call","job.submit","state.get","state.set","license.get","ui.print","coedit.join","coedit.subscribe","coedit.append","coedit.lease","coedit.cursor","coedit.blob.put","coedit.blob.get","coedit.leave"],h={maxTextBytes:33554432,maxStateBytes:8192,maxLineChars:300,connectTimeoutMs:1e4,maxDownloadBytes:268435456,maxPrintBytes:67108864,maxEncryptedBytes:209715200};function R(n){return!!n&&typeof n=="object"&&n.type===S&&typeof n.v=="number"}function L(n){return!!n&&typeof n=="object"&&n.type===B}function x(n){return!!n&&typeof n=="object"&&typeof n.id=="number"&&typeof n.method=="string"}function I(n){return O.includes(n)}class p extends Error{constructor(f){super(f.message||f.code),this.name="FilexError",this.code=f.code}}let E=null;function T(n={}){return E||(E=j(n)),E}function j(n){return new Promise((f,m)=>{if(typeof window>"u"||window.parent===window){m(new p({code:"unavailable",message:"not running inside filex"}));return}const a=window.parent,d=P(),c=setTimeout(()=>{window.removeEventListener("message",b),m(new p({code:"unavailable",message:"filex did not answer"}))},n.timeoutMs??h.connectTimeoutMs);function b(g){if(g.source!==a||!L(g.data)||!g.ports[0]||d&&g.origin!==d)return;window.removeEventListener("message",b),clearTimeout(c);const r=g.ports[0],u=k(r,n);u.request("session.get").then(l=>{u.session=l,n.applyTheme!==!1&&(M(l.theme,l.locale,l.dir),u.on("theme",y=>M(y)),u.on("locale",y=>{const e=y;M(void 0,e.locale,e.dir)})),f(u)}).catch(m)}window.addEventListener("message",b),a.postMessage({type:S,v:1},"*")})}function P(){try{const n=window.location.ancestorOrigins;if(n&&n.length>0&&n[0]!=="null")return n[0]}catch{}return""}function k(n,f){let m=0;const a=new Map,d=new Map;let c=null;n.onmessage=e=>{const t=e.data;if(!(!t||typeof t!="object")){if(typeof t.id=="number"){const s=a.get(t.id);if(!s)return;a.delete(t.id),t.error?s.reject(new p(t.error)):s.resolve(t.result);return}if(typeof t.hid=="number"){b(t);return}if(typeof t.event=="string")for(const s of d.get(t.event)??[])try{s(t.data)}catch(i){console.error("[filex-app-ui] listener failed",i)}}};async function b(e){if(e.request==="save"){if(!c){n.postMessage({hid:e.hid,error:{code:"unavailable",message:"this app has no save handler"}});return}try{const t=await g();n.postMessage({hid:e.hid,result:t??{saved:!0}})}catch(t){const s=t instanceof p?{code:t.code,message:t.message}:{code:"failed",message:String(t?.message??t)};n.postMessage({hid:e.hid,error:s})}return}n.postMessage({hid:e.hid,error:{code:"unknown_method"}})}async function g(){if(!c)return null;const e=await c();return e==null?null:y.save(e)}function r(e,t,s=[]){const i=++m;return new Promise((w,v)=>{a.set(i,{resolve:w,reject:v});try{n.postMessage({id:i,method:e,params:t},s)}catch(D){a.delete(i),v(D)}})}function u(e,t){r(e,t).catch(s=>console.warn("[filex-app-ui]",e,s))}async function l(e){if(typeof e=="string")return{data:e,transfer:[]};if(e instanceof ArrayBuffer)return{data:e,transfer:[e]};if(ArrayBuffer.isView(e)){const t=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);return{data:t,transfer:[t]}}if(typeof Blob<"u"&&e instanceof Blob){const t=await e.arrayBuffer();return{data:t,transfer:[t]}}if(typeof ReadableStream<"u"&&e instanceof ReadableStream)return{data:e,transfer:[e]};throw new p({code:"invalid",message:"save takes a string, a Blob, bytes or a ReadableStream"})}const y={session:void 0,async open(e=0){const t=y.session?.files?.[e];if(!t)throw new p({code:"not_found",message:`no file at ${e}`});const s=(i,w)=>r("file.read",{index:e,as:i}).then(v=>v[w]);return{...t,text:()=>s("text","text"),bytes:()=>s("bytes","bytes"),stream:()=>s("stream","stream"),read:(i="bytes")=>r("file.read",{index:e,as:i}),save:(i,w)=>y.save(i,{index:e,mime:w})}},async save(e,t={}){const s=await l(e),i={index:t.index??0,data:s.data,mime:t.mime};return typeof t.through=="number"&&(i.through=t.through),t.csv&&(i.csv={...t.csv}),t.beside&&(i.beside=t.beside),t.conflict&&(i.conflict=t.conflict),r("file.save",i,s.transfer)},async saveAs(e,t,s){const i=await l(t);return r("file.saveAs",{name:e,data:i.data,mime:s},i.transfer)},async download(e,t,s){const i=await l(t);return r("ui.download",{name:e,data:i.data,mime:s},i.transfer)},async print(e,t){const s=await l(t);return r("ui.print",{name:e,data:s.data,mime:"application/pdf"},s.transfer)},coedit:{join(e=0){return r("coedit.join",{index:e})},subscribe(e){return r("coedit.subscribe",{from:e})},append(e,t){return r("coedit.append",{kind:e,body:t})},async lease(e,t){return!!(await r("coedit.lease",{op:e,changesSeen:t}))?.granted},cursor(e){u("coedit.cursor",{cursor:e})},async putBlob(e,t){const s=await l(t);if(!(s.data instanceof ArrayBuffer))throw new p({code:"invalid",message:"a blob is bytes"});await r("coedit.blob.put",{name:e,data:s.data},s.transfer)},async getBlob(e){return(await r("coedit.blob.get",{name:e})).bytes},async leave(){await r("coedit.leave")}},dirty(e){u("ui.dirty",{dirty:!!e})},title(e){u("ui.title",{text:String(e).slice(0,h.maxLineChars)})},toast(e,t){u("ui.toast",{text:String(e).slice(0,h.maxLineChars),tone:t})},confirm(e){return r("ui.confirm",typeof e=="string"?{text:e}:e)},close(){u("ui.close")},copy(e){return r("clipboard.write",{text:String(e)})},call(e,t){return r("engine.call",{method:e,params:t})},submit(e,t){return r("job.submit",{action:e,params:t})},state:{get(e){return r("state.get",{key:e})},set(e,t){return r("state.set",{key:e,value:t})}},license:{get(){return r("license.get")}},on(e,t){let s=d.get(e);return s||d.set(e,s=new Set),s.add(t),()=>s.delete(t)},onSave(e){return c=e,()=>{c===e&&(c=null)}},request:r};return f.saveShortcut!==!1&&typeof document<"u"&&document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey&&(e.key==="s"||e.key==="S")&&c&&(e.preventDefault(),g().catch(t=>y.toast(String(t?.message??t),"error")))},!0),y}function M(n,f,m){if(typeof document>"u")return;const a=document.documentElement;if(n){a.dataset.theme=n.mode,a.style.colorScheme=n.mode;for(const[d,c]of Object.entries(n.tokens??{}))/^--fe-[a-z0-9-]+$/.test(d)&&typeof c=="string"&&a.style.setProperty(d,c)}f&&(a.lang=f),m&&(a.dir=m)}return o.BRIDGE_VERSION=1,o.FilexError=p,o.HELLO=S,o.LIMITS=h,o.METHODS=O,o.PORT=B,o.connect=T,o.isHello=R,o.isKnownMethod=I,o.isPortMessage=L,o.isRequest=x,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),o}({});
|
|
2
2
|
//# sourceMappingURL=filex-app-ui.iife.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filex-app-ui.iife.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get'\n /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */\n | 'ui.print'\n /**\n * Editing together (`coedit.*`, task #189): the session's log the host\n * seals, orders through filex's relay and opens again. Defined in filex\n * 0.55, which answers each `unavailable`; offered from 0.56 to an app\n * granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.\n */\n | 'coedit.join'\n | 'coedit.subscribe'\n | 'coedit.append'\n | 'coedit.lease'\n | 'coedit.cursor'\n | 'coedit.blob.put'\n | 'coedit.blob.get'\n | 'coedit.leave';\n\nexport const METHODS: readonly Method[] = [\n 'session.get',\n 'file.read',\n 'file.save',\n 'file.saveAs',\n 'ui.dirty',\n 'ui.title',\n 'ui.toast',\n 'ui.confirm',\n 'ui.close',\n 'clipboard.write',\n 'ui.download',\n 'engine.call',\n 'job.submit',\n 'state.get',\n 'state.set',\n 'license.get',\n 'ui.print',\n 'coedit.join',\n 'coedit.subscribe',\n 'coedit.append',\n 'coedit.lease',\n 'coedit.cursor',\n 'coedit.blob.put',\n 'coedit.blob.get',\n 'coedit.leave',\n];\n\n/** What the host tells the app, unasked. */\nexport type HostEvent =\n /** The interface's colours changed (light/dark, palette). `data: Theme`. */\n | 'theme'\n /** The reader's language changed. `data: {locale, dir}`. */\n | 'locale'\n /** The open file changed underneath (another tab, another person). */\n | 'file.changed'\n /** The person closed the frame's surroundings; last chance to say so. */\n | 'close.request'\n /** An administrator installed a new version of this app: reload to use it. */\n | 'app.updated'\n /** Editing together: the next entry of the session's log, opened. `data: CoEditEntry`. */\n | 'coedit.entry'\n /** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */\n | 'coedit.cursor'\n /**\n * Editing together: the host stopped handing entries over. `data:\n * CoEditDropped` - `gone`: the membership ended (join again); `broken`: an\n * entry did not check out (stop editing together). A connection that drops\n * for a moment is the host's own: it reconnects and says nothing.\n */\n | 'coedit.dropped';\n\n/** What the host may ask the app (and wait for). */\nexport type HostRequest =\n /** Save now (the host's Save button, a draft's \"Save to disk\"): the app\n * hands its document to `file.save` and answers when it is written. */\n 'save';\n\n/** Why a call was refused. */\nexport type ErrorCode =\n /** The app was not granted what the call needs (files:write for a save…). */\n | 'not_granted'\n /** No such file (an index past the opened files), no such key. */\n | 'not_found'\n /** The file cannot be written: a read-only storage, a view-only opening. */\n | 'read_only'\n /** The call's parameters are wrong. */\n | 'invalid'\n /** Over a limit (LIMITS). */\n | 'too_large'\n /** The server could not be reached or refused; `message` says what. */\n | 'failed'\n /** The person said no (a confirm, a save-as picker closed). */\n | 'cancelled'\n /** The host does not offer this here (no engine, no save handler). */\n | 'unavailable'\n | 'unknown_method';\n\nexport interface BridgeError {\n code: ErrorCode;\n message?: string;\n}\n\n/* ── messages ──────────────────────────────────────────────────────────── */\n\nexport interface HelloMessage {\n type: typeof HELLO;\n v: number;\n}\n\nexport interface PortMessage {\n type: typeof PORT;\n v: number;\n}\n\nexport interface RequestMessage {\n id: number;\n method: Method;\n params?: unknown;\n}\n\nexport interface ResponseMessage {\n id: number;\n result?: unknown;\n error?: BridgeError;\n}\n\nexport interface EventMessage {\n event: HostEvent;\n data?: unknown;\n}\n\nexport interface HostRequestMessage {\n hid: number;\n request: HostRequest;\n params?: unknown;\n}\n\nexport interface HostResponseMessage {\n hid: number;\n result?: unknown;\n error?: BridgeError;\n}\n\n/* ── shapes ────────────────────────────────────────────────────────────── */\n\n/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */\nexport interface Theme {\n mode: 'light' | 'dark';\n /** `--fe-bg`, `--fe-text`, `--fe-accent`… — the SDK sets them on `<html>`. */\n tokens: Record<string, string>;\n}\n\n/** One file the interface was opened with. Never a storage path. */\nexport interface FileInfo {\n index: number;\n name: string;\n /** Lower-case, no dot. */\n ext: string;\n size: number;\n mime: string;\n /** It cannot be saved over: read-only storage, view-only opening, or the\n * app holds no `files:write`. */\n readOnly: boolean;\n /**\n * 0.55.0: the file is end-to-end encrypted - `folder` in an encrypted\n * folder, `vault` in a vault (where only one person edits at a time),\n * `file` for a single encrypted file (`.fxe`). As the server says it;\n * ABSENT for a file that is not encrypted.\n *\n * ⚠ The server holds such a file only as ciphertext and has no key. Unless\n * `plaintext` is true, filex hands the app nothing of it: `file.read` is\n * refused (`failed` with `encrypted`), a save is refused, and `readOnly`\n * is true.\n */\n encrypted?: 'folder' | 'vault' | 'file';\n /**\n * 0.56.0: filex hands this encrypted file to the app in the clear. The\n * person's browser, which holds the folder key while the folder is\n * unlocked, decrypts it for `file.read` and encrypts what `file.save`\n * hands over before it leaves the browser; the server sees only\n * ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app\n * whose manifest says `encrypted_folders: {\"open\": true}` (the grant\n * `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.\n *\n * In a vault (`encrypted: 'vault'`) the browser reads the file from the\n * vault's packs and writes a save as the vault's next generation, under\n * the vault's write lock (one writer at a time). A save is refused -\n * `failed` with `vault_locked` - while somebody else writes the vault, and\n * with `vault_lock_lost` when this session's write lock ended before the\n * save was committed: nothing was written, save again. The vault locked\n * meanwhile (its keys left the browser): `unavailable`, and filex closes\n * the app.\n *\n * What changes for the app: a read and a save are held whole in the page\n * (at most LIMITS.maxEncryptedBytes); a save is written only over the file\n * as it was opened (`failed` with `changed` when it changed meanwhile);\n * `file.saveAs` answers `unavailable` (a copy elsewhere would leave the\n * folder decrypted); the app's module (`engine.call`, `job.submit`) is\n * never handed the file; `state.get` / `state.set` use a store of their\n * own for this folder, sealed in the person's browser (the server keeps it\n * and cannot read it; apart from the app's ordinary store, a little smaller\n * as it is measured sealed). Everything the app holds is plaintext: keep it\n * in the frame. In a folder whose names are encrypted too, `name` and\n * `ext` are the decrypted name's - the server never sees them.\n */\n plaintext?: true;\n}\n\n/** What `session.get` answers — what the app knows about where it runs. */\nexport interface Session {\n v: number;\n app: { name: string; version: string };\n view: { id: string; placement: 'modal' | 'page' | 'inspector' | 'home' | 'viewer' };\n /** The reader's language tag (`tr`, `pt-br`) and its direction. */\n locale: string;\n dir: 'ltr' | 'rtl';\n theme: Theme;\n /** The person's display name; never their e-mail, never a token. */\n user: { name: string };\n files: FileInfo[];\n /** The permissions this app was granted — so it can hide what it may not do. */\n grants: string[];\n /** The administrator's non-secret settings for this app (grant `settings`). */\n settings?: Record<string, string>;\n}\n\n/** `file.read` params. */\nexport interface ReadParams {\n index?: number;\n /** `stream` (a transferred ReadableStream<Uint8Array>), `bytes` (an\n * ArrayBuffer), or `text` (UTF-8, at most LIMITS.maxTextBytes). */\n as?: 'stream' | 'bytes' | 'text';\n}\n\nexport interface ReadResult {\n name: string;\n size: number;\n mime: string;\n stream?: ReadableStream<Uint8Array>;\n bytes?: ArrayBuffer;\n text?: string;\n}\n\n/** `file.save` params: the new content of the opened file. */\nexport interface SaveParams {\n index?: number;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n /**\n * 0.55.0, editing together: the last log entry (`CoEditEntry.seq`) the\n * saved document holds, so the session knows what is saved. Ignored\n * outside a session.\n */\n through?: number;\n}\n\nexport interface SaveResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.download`: a file for the person's own disk. filex does it (a sandboxed\n * frame cannot download), with the app's `ui:download` grant, on a gesture in\n * the frame or the person's yes, never over LIMITS.maxDownloadBytes.\n */\nexport interface DownloadParams {\n /** The file name offered — a name, not a path. */\n name: string;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface DownloadResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.print` (filex 0.55.0): a PDF for the browser's print dialog. A\n * sandboxed frame may not open that dialog (measured: Chromium ignores\n * `print()` there and says so, Firefox ignores it), so filex prints the PDF\n * from a page of its own, with the app's `ui:print` grant, never over\n * LIMITS.maxPrintBytes. filex asks every time, above the frame, and the\n * dialog opens only on the person's click on filex's Allow. The print dialog\n * can save the PDF too, so `ui:print` is the same kind of grant as\n * `ui:download`. filex 0.54 and older answer `unknown_method`.\n */\nexport interface PrintParams {\n /** The document's name, for the question filex may ask - a name, not a path. */\n name: string;\n /** The PDF: a transferred ArrayBuffer or ReadableStream<Uint8Array>. */\n data: ReadableStream<Uint8Array> | ArrayBuffer;\n /** `application/pdf` when given; nothing else is printed. */\n mime?: string;\n}\n\nexport interface PrintResult {\n /** The browser's print dialog was opened with the PDF. */\n printed: true;\n size: number;\n}\n\n/* ── editing together (coedit.*: filex 0.55 defined, 0.56 offers) ──────── */\n\n/** What a member appends: the editor's changes, a lock request, a release. */\nexport type CoEditKind = 'changes' | 'lock' | 'release';\n\n/** A member of the session. */\nexport interface CoEditMember {\n /** The relay's id for the connection. */\n client: string;\n /** An opaque id of the person, the same for each of their connections in\n * this session; never an account id. */\n user: string;\n /** The person's display name. */\n name: string;\n /** The editor's per-session user index: given once, never twice. */\n indexUser: number;\n /** May append changes (an editor role and the right to change the file). */\n canEdit: boolean;\n}\n\n/** `coedit.join` params: join (or start) the session of an opened file. */\nexport interface CoEditJoinParams {\n index?: number;\n}\n\n/**\n * What `coedit.join` answers: who the app is in the session and how far\n * the log has gone. The entries come as `coedit.entry` events once the app\n * subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`\n * when filex offers no editing together here (filex 0.55, editing together\n * turned off, an encrypted file, a vault): the app edits alone - the message\n * says why (`not_ready` after waiting for a session's creator, `encrypted`,\n * `nothing_to_watch` for a person who may only read the file while nobody\n * edits it, or the server's code: `coedit_off`, `coedit_build`,\n * `coedit_file_changed`).\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /**\n * The last entry's seq when filex handed the session over (0: an empty\n * log). filex 0.56: the changes this browser kept from an earlier opening\n * of the file (a lost connection, a closed tab), which filex sends before\n * it answers, are in the log up to here - an editor that starts once it\n * has read this far opens with them.\n */\n head: number;\n /** The last `changes` entry's seq, as far as `head`. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\n /**\n * filex 0.56: this member started the session. Before anybody else can\n * join it puts the base document - EXACTLY the bytes it opened - with\n * `coedit.blob.put('base', bytes)`. A member that did not start it opens\n * `coedit.blob.get('base')`, never the file: the file may hold a save of\n * the session already, and the log applies to the base.\n */\n created: boolean;\n}\n\n/** `coedit.dropped` event data (filex 0.56). */\nexport interface CoEditDropped {\n /** The last entry the host handed over. */\n from: number;\n /** `gone`: the membership ended (the session closed, or the page was away\n * too long) - join again. `broken`: an entry did not check out (missing,\n * moved, altered, replayed) - stop editing together. */\n reason: 'gone' | 'broken';\n}\n\n/** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */\nexport interface CoEditSubscribeParams {\n from: number;\n}\n\n/** `coedit.append` params: the host seals it and the relay places it. */\nexport interface CoEditAppendParams {\n kind: CoEditKind;\n body: unknown;\n}\n\nexport interface CoEditAppendResult {\n /** Where it landed; it comes back as a `coedit.entry` too. */\n seq: number;\n}\n\n/** `coedit.lease`: the changes lease (one writer at a time, one that has\n * seen every change). */\nexport interface CoEditLeaseParams {\n op: 'acquire' | 'release';\n /** The seq of the last `changes` entry the app applied. */\n changesSeen: number;\n}\n\nexport interface CoEditLeaseResult {\n granted: boolean;\n}\n\n/** `coedit.cursor` params: passed to the other members, not kept. The host\n * sends a few a second at most (the last one wins). */\nexport interface CoEditCursorParams {\n cursor: unknown;\n}\n\n/** `coedit.blob.put` / `coedit.blob.get`: the session's sealed blobs (the\n * base document, images added while editing). A name, not a path. */\nexport interface CoEditBlobPutParams {\n name: string;\n data: ArrayBuffer;\n}\n\nexport interface CoEditBlobGetParams {\n name: string;\n}\n\nexport interface CoEditBlobGetResult {\n name: string;\n bytes: ArrayBuffer;\n}\n\ninterface CoEditEntryBase {\n seq: number;\n /** The relay's time, Unix ms - the same for every member. */\n at: number;\n client: string;\n}\n\n/** One entry of the session's log, opened and checked by filex. */\nexport type CoEditEntry =\n | (CoEditEntryBase & { kind: CoEditKind; body: unknown })\n | (CoEditEntryBase & { kind: 'join'; member: CoEditMember })\n | (CoEditEntryBase & { kind: 'leave' })\n | (CoEditEntryBase & { kind: 'saved'; through: number });\n\n/** `coedit.cursor` event data. */\nexport interface CoEditCursor {\n client: string;\n cursor: unknown;\n}\n\n/**\n * `file.saveAs`: a NEW file, in a folder the person picks in filex's own\n * folder dialog (the one its Move to… uses), which opens in the opened\n * file's folder. Closing the dialog answers `cancelled`.\n */\nexport interface SaveAsParams {\n /** The file name — a name, not a path. */\n name: string;\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveAsResult {\n saved: true;\n name: string;\n size: number;\n}\n\nexport interface ToastParams {\n text: string;\n tone?: 'info' | 'success' | 'warning' | 'error';\n}\n\nexport interface ConfirmParams {\n title?: string;\n text: string;\n confirm?: string;\n cancel?: string;\n danger?: boolean;\n}\n\n/**\n * `license.get` (filex 0.52.0): what the app's license says. A paid app is\n * installed from a store, which issues its license and is asked about it every\n * day; filex keeps the key and never hands it to the app. `status`:\n *\n * - `free`: not a paid app;\n * - `valid`: the store said valid at its last check;\n * - `grace`: valid, but the store could not be asked since - it holds until\n * the grace the store signed ends;\n * - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,\n * `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held\n * and its interface is not served, so an app rarely reads one of these.\n *\n * `valid_until` / `updates_until` are there for a valid license (dates as\n * RFC 3339). Who holds the license is not the app's to read. filex 0.51.0\n * and older answer `unknown_method`.\n */\nexport interface LicenseInfo {\n status: string;\n valid_until?: string;\n updates_until?: string;\n}\n\n/** `engine.call`: the app's own module, `ui_call` export. */\nexport interface EngineCallParams {\n method: string;\n params?: unknown;\n}\n\n/** `job.submit`: queue one of the app's actions on the opened files. */\nexport interface JobSubmitParams {\n action: string;\n params?: Record<string, unknown>;\n}\n\n/* ── limits ────────────────────────────────────────────────────────────── */\n\nexport const LIMITS = {\n /** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */\n maxTextBytes: 32 << 20,\n /** One `state.set` value, as JSON. */\n maxStateBytes: 8 << 10,\n /** A `ui.toast` / `ui.title` text, in characters. */\n maxLineChars: 300,\n /** How long the SDK waits for the host's port. */\n connectTimeoutMs: 10_000,\n /** The largest file `ui.download` hands the person (streamed or not). */\n maxDownloadBytes: 256 << 20,\n /**\n * The largest PDF `ui.print` prints (filex 0.55). Lower than a download:\n * a download can stream to disk, a print is held whole in the page (one\n * Blob) until the print dialog has read it, and a document worth printing\n * is far smaller.\n */\n maxPrintBytes: 64 << 20,\n /**\n * The largest file of an encrypted folder filex hands an app in the clear\n * or takes back from it (filex 0.56, `FileInfo.plaintext`): it is\n * decrypted and encrypted whole in the page, as filex's own explorer does\n * for a file of this size.\n */\n maxEncryptedBytes: 200 << 20,\n} as const;\n\n/* ── guards ────────────────────────────────────────────────────────────── */\n\nexport function isHello(d: unknown): d is HelloMessage {\n return !!d && typeof d === 'object' && (d as HelloMessage).type === HELLO && typeof (d as HelloMessage).v === 'number';\n}\n\nexport function isPortMessage(d: unknown): d is PortMessage {\n return !!d && typeof d === 'object' && (d as PortMessage).type === PORT;\n}\n\nexport function isRequest(d: unknown): d is RequestMessage {\n return (\n !!d &&\n typeof d === 'object' &&\n typeof (d as RequestMessage).id === 'number' &&\n typeof (d as RequestMessage).method === 'string'\n );\n}\n\nexport function isKnownMethod(m: string): m is Method {\n return (METHODS as readonly string[]).includes(m);\n}\n","/**\n * @brftech/filex-app-ui — the SDK an app's own interface bundles.\n *\n * ```js\n * import { connect } from '@brftech/filex-app-ui';\n *\n * const fx = await connect(); // the handshake with filex\n * const file = await fx.open(); // the file the app was opened with\n * editor.setValue(await file.text());\n * editor.onChange(() => fx.dirty(true));\n * fx.onSave(async () => editor.getValue()); // filex's Save, a draft's \"Save to disk\", Ctrl+S\n * ```\n *\n * No dependency, a few KB. The interface runs in a sandboxed frame with no\n * network and no storage of its own: every call below goes to filex over the\n * MessagePort the handshake set up, and filex decides it (docs/APP-PLUGINS.md\n * → \"An app's own interface\"). The protocol itself is `./protocol`.\n */\nimport {\n BRIDGE_VERSION,\n HELLO,\n LIMITS,\n isPortMessage,\n type BridgeError,\n type ConfirmParams,\n type ErrorCode,\n type FileInfo,\n type HostEvent,\n type HostRequestMessage,\n type Method,\n type ReadResult,\n type SaveAsResult,\n type SaveResult,\n type Session,\n type Theme,\n type ToastParams,\n type DownloadResult,\n type LicenseInfo,\n type PrintResult,\n type CoEditHello,\n type CoEditKind,\n type CoEditAppendResult,\n type CoEditLeaseResult,\n type CoEditBlobGetResult,\n} from './protocol';\n\nexport * from './protocol';\n\n/** A call filex refused, with the reason as a code. */\nexport class FilexError extends Error {\n readonly code: ErrorCode;\n constructor(err: BridgeError) {\n super(err.message || err.code);\n this.name = 'FilexError';\n this.code = err.code;\n }\n}\n\n/** Content the app hands to a save. */\nexport type SaveData = string | Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>;\n\n/** One opened file, read the way the app needs it. */\nexport interface OpenedFile extends FileInfo {\n text(): Promise<string>;\n bytes(): Promise<ArrayBuffer>;\n stream(): Promise<ReadableStream<Uint8Array>>;\n /** Save new content over this file (a new version, or the draft it is). */\n save(data: SaveData, mime?: string): Promise<SaveResult>;\n}\n\nexport interface ConnectOptions {\n /** How long to wait for filex (default 10 s). */\n timeoutMs?: number;\n /**\n * Put filex's colours (`--fe-*`), `lang`, `dir` and `data-theme` on\n * `<html>`, and keep them current (default true).\n */\n applyTheme?: boolean;\n /** Ctrl/Cmd+S inside the interface runs the save handler (default true). */\n saveShortcut?: boolean;\n}\n\nexport interface FilexApp {\n readonly session: Session;\n /** The file at `index` (default the first) the interface was opened with. */\n open(index?: number): Promise<OpenedFile>;\n /**\n * Save new content over an opened file. `through` (filex 0.56, editing\n * together): the last log entry the saved document holds, so the session\n * knows what is saved.\n */\n save(data: SaveData, opts?: { index?: number; mime?: string; through?: number }): Promise<SaveResult>;\n /** Save a NEW file; filex asks the person where (its own folder picker). */\n saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;\n /** Unsaved changes, or not: filex asks before the person leaves them. */\n dirty(on: boolean): void;\n title(text: string): void;\n toast(text: string, tone?: ToastParams['tone']): void;\n confirm(opts: ConfirmParams | string): Promise<boolean>;\n /** Ask filex to close the interface (it asks first when there are changes). */\n close(): void;\n /** Put text on the clipboard (filex does it, the same way in every browser). */\n copy(text: string): Promise<void>;\n /**\n * Hand the person a file for their own disk (the app needs `ui.download`\n * in its manifest). Call it from a click or a key press: without one,\n * filex asks the person first. `cancelled` when they say no.\n */\n download(name: string, data: SaveData, mime?: string): Promise<DownloadResult>;\n /**\n * Print a PDF (filex 0.55; the app needs `ui.print` in its manifest): a\n * sandboxed frame may not open the print dialog, so filex prints it from\n * its own page. filex asks the person every time, and the dialog opens on\n * their click on filex's Allow; at most LIMITS.maxPrintBytes. filex 0.54\n * and older answer `unknown_method`, and a host whose print page may not\n * be shown `unavailable`: hand the PDF over with `download` then.\n */\n print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;\n /**\n * Editing together (filex 0.56; the app's manifest says `\"co_edit\":\n * {\"open\": true}`, granted as `files:co-edit`). filex seals what the app appends, orders it\n * through its relay and hands every member the same entries in the same\n * order (`coedit.entry` events after `subscribe`); the app never holds a\n * key. `join` answering `unavailable` (filex 0.55, editing together off\n * here, an encrypted file) means: edit alone. The member that started the\n * session (`created`) puts the base document before anybody else may join.\n */\n coedit: {\n join(index?: number): Promise<CoEditHello>;\n subscribe(from: number): Promise<void>;\n append(kind: CoEditKind, body: unknown): Promise<CoEditAppendResult>;\n lease(op: 'acquire' | 'release', changesSeen: number): Promise<boolean>;\n cursor(cursor: unknown): void;\n putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView): Promise<void>;\n getBlob(name: string): Promise<ArrayBuffer>;\n leave(): Promise<void>;\n };\n /** Call the app's own module (`ui_call` export) with the opened files. */\n call<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Queue one of the app's actions on the opened files; answers the op. */\n submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;\n /**\n * This person's small store for this app (JSON values, 8 KiB each). While\n * the app holds a file of an encrypted folder in the clear\n * (`FileInfo.plaintext`, filex 0.56) it is another store, sealed in the\n * browser - apart from this one, and empty while the folder is locked.\n */\n state: {\n get<T = unknown>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n };\n /**\n * The app's license (filex 0.52.0; a paid app installed from a store):\n * `{status: \"free\"}` for a free app, `{status: \"valid\", valid_until,\n * updates_until}` for a licensed one. Never the key, never the licensee.\n */\n license: {\n get(): Promise<LicenseInfo>;\n };\n /** Listen to filex. Returns the unsubscribe. */\n on(event: HostEvent, handler: (data: unknown) => void): () => void;\n /**\n * What to save when filex asks (its Save button, a draft's \"Save to disk\",\n * Ctrl+S): return the document and the SDK saves it over the opened file;\n * return nothing if the handler saved by itself.\n */\n onSave(handler: () => SaveData | void | Promise<SaveData | void>): () => void;\n /** Raw call, for a method this SDK has no helper for. */\n request<T = unknown>(method: Method, params?: unknown, transfer?: Transferable[]): Promise<T>;\n}\n\nlet connecting: Promise<FilexApp> | null = null;\n\n/**\n * The handshake. Resolves once filex answered with its port and the session;\n * rejects outside filex or when filex did not answer in time. Calling it again\n * answers the same connection.\n */\nexport function connect(opts: ConnectOptions = {}): Promise<FilexApp> {\n if (!connecting) connecting = open(opts);\n return connecting;\n}\n\nfunction open(opts: ConnectOptions): Promise<FilexApp> {\n return new Promise<FilexApp>((resolve, reject) => {\n if (typeof window === 'undefined' || window.parent === window) {\n reject(new FilexError({ code: 'unavailable', message: 'not running inside filex' }));\n return;\n }\n const parent = window.parent;\n // Where filex runs, when the browser says so (Chrome, Safari). A frame\n // drawn with referrerpolicy=\"no-referrer\" has no document.referrer, and\n // Firefox has no ancestorOrigins: then `source === parent` is the check,\n // which is the one that matters (only the parent window IS the parent).\n const expected = ancestorOrigin();\n const timer = setTimeout(() => {\n window.removeEventListener('message', onMessage);\n reject(new FilexError({ code: 'unavailable', message: 'filex did not answer' }));\n }, opts.timeoutMs ?? LIMITS.connectTimeoutMs);\n\n function onMessage(ev: MessageEvent) {\n if (ev.source !== parent || !isPortMessage(ev.data) || !ev.ports[0]) return;\n if (expected && ev.origin !== expected) return;\n window.removeEventListener('message', onMessage);\n clearTimeout(timer);\n const port = ev.ports[0];\n const app = bridge(port, opts);\n app\n .request<Session>('session.get')\n .then((session) => {\n (app as { session: Session }).session = session;\n if (opts.applyTheme !== false) {\n applyLook(session.theme, session.locale, session.dir);\n app.on('theme', (t) => applyLook(t as Theme));\n app.on('locale', (l) => {\n const v = l as { locale?: string; dir?: 'ltr' | 'rtl' };\n applyLook(undefined, v.locale, v.dir);\n });\n }\n resolve(app);\n })\n .catch(reject);\n }\n window.addEventListener('message', onMessage);\n parent.postMessage({ type: HELLO, v: BRIDGE_VERSION }, '*');\n });\n}\n\nfunction ancestorOrigin(): string {\n try {\n const list = (window.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins;\n if (list && list.length > 0 && list[0] !== 'null') return list[0];\n } catch {\n /* no such API */\n }\n return '';\n}\n\nfunction bridge(port: MessagePort, opts: ConnectOptions): FilexApp {\n let seq = 0;\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();\n const listeners = new Map<HostEvent, Set<(data: unknown) => void>>();\n let saveHandler: (() => SaveData | void | Promise<SaveData | void>) | null = null;\n\n port.onmessage = (ev: MessageEvent) => {\n const m = ev.data as Record<string, unknown> | null;\n if (!m || typeof m !== 'object') return;\n if (typeof m.id === 'number') {\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n if (m.error) p.reject(new FilexError(m.error as BridgeError));\n else p.resolve(m.result);\n return;\n }\n if (typeof m.hid === 'number') {\n void answerHost(m as unknown as HostRequestMessage);\n return;\n }\n if (typeof m.event === 'string') {\n for (const h of listeners.get(m.event as HostEvent) ?? []) {\n try {\n h(m.data);\n } catch (e) {\n console.error('[filex-app-ui] listener failed', e);\n }\n }\n }\n };\n\n async function answerHost(m: HostRequestMessage) {\n if (m.request === 'save') {\n if (!saveHandler) {\n port.postMessage({ hid: m.hid, error: { code: 'unavailable', message: 'this app has no save handler' } });\n return;\n }\n try {\n const res = await runSave();\n port.postMessage({ hid: m.hid, result: res ?? { saved: true } });\n } catch (e) {\n const err = e instanceof FilexError ? { code: e.code, message: e.message } : { code: 'failed', message: String((e as Error)?.message ?? e) };\n port.postMessage({ hid: m.hid, error: err });\n }\n return;\n }\n port.postMessage({ hid: m.hid, error: { code: 'unknown_method' } });\n }\n\n async function runSave(): Promise<SaveResult | null> {\n if (!saveHandler) return null;\n const data = await saveHandler();\n if (data === undefined || data === null) return null;\n return app.save(data as SaveData);\n }\n\n function request<T>(method: Method, params?: unknown, transfer: Transferable[] = []): Promise<T> {\n const id = ++seq;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject });\n try {\n port.postMessage({ id, method, params }, transfer);\n } catch (e) {\n pending.delete(id);\n reject(e);\n }\n });\n }\n\n function notify(method: Method, params?: unknown) {\n request(method, params).catch((e) => console.warn('[filex-app-ui]', method, e));\n }\n\n async function payload(data: SaveData): Promise<{ data: ReadableStream<Uint8Array> | ArrayBuffer | string; transfer: Transferable[] }> {\n if (typeof data === 'string') return { data, transfer: [] };\n if (data instanceof ArrayBuffer) return { data, transfer: [data] };\n if (ArrayBuffer.isView(data)) {\n const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n return { data: copy, transfer: [copy] };\n }\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n const buf = await data.arrayBuffer();\n return { data: buf, transfer: [buf] };\n }\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {\n return { data, transfer: [data as unknown as Transferable] };\n }\n throw new FilexError({ code: 'invalid', message: 'save takes a string, a Blob, bytes or a ReadableStream' });\n }\n\n const app: FilexApp = {\n session: undefined as unknown as Session,\n async open(index = 0) {\n const info = app.session?.files?.[index];\n if (!info) throw new FilexError({ code: 'not_found', message: `no file at ${index}` });\n const read = <K extends keyof ReadResult>(as: 'stream' | 'bytes' | 'text', key: K) =>\n request<ReadResult>('file.read', { index, as }).then((r) => r[key] as NonNullable<ReadResult[K]>);\n return {\n ...info,\n text: () => read('text', 'text'),\n bytes: () => read('bytes', 'bytes'),\n stream: () => read('stream', 'stream'),\n save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),\n };\n },\n async save(data, o = {}) {\n const p = await payload(data);\n const params: Record<string, unknown> = { index: o.index ?? 0, data: p.data, mime: o.mime };\n if (typeof o.through === 'number') params.through = o.through;\n return request<SaveResult>('file.save', params, p.transfer);\n },\n async saveAs(name, data, mime) {\n const p = await payload(data);\n return request<SaveAsResult>('file.saveAs', { name, data: p.data, mime }, p.transfer);\n },\n async download(name, data, mime) {\n const p = await payload(data);\n return request<DownloadResult>('ui.download', { name, data: p.data, mime }, p.transfer);\n },\n async print(name, pdf) {\n const p = await payload(pdf);\n return request<PrintResult>('ui.print', { name, data: p.data, mime: 'application/pdf' }, p.transfer);\n },\n coedit: {\n join(index = 0) {\n return request<CoEditHello>('coedit.join', { index });\n },\n subscribe(from: number) {\n return request<void>('coedit.subscribe', { from });\n },\n append(kind: CoEditKind, body: unknown) {\n return request<CoEditAppendResult>('coedit.append', { kind, body });\n },\n async lease(op: 'acquire' | 'release', changesSeen: number) {\n const r = await request<CoEditLeaseResult>('coedit.lease', { op, changesSeen });\n return !!r?.granted;\n },\n cursor(cursor: unknown) {\n notify('coedit.cursor', { cursor });\n },\n async putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView) {\n const p = await payload(data);\n if (!(p.data instanceof ArrayBuffer)) throw new FilexError({ code: 'invalid', message: 'a blob is bytes' });\n await request<void>('coedit.blob.put', { name, data: p.data }, p.transfer);\n },\n async getBlob(name: string) {\n const r = await request<CoEditBlobGetResult>('coedit.blob.get', { name });\n return r.bytes;\n },\n async leave() {\n await request<void>('coedit.leave');\n },\n },\n dirty(on) {\n notify('ui.dirty', { dirty: !!on });\n },\n title(text) {\n notify('ui.title', { text: String(text).slice(0, LIMITS.maxLineChars) });\n },\n toast(text, tone) {\n notify('ui.toast', { text: String(text).slice(0, LIMITS.maxLineChars), tone });\n },\n confirm(o) {\n const params = typeof o === 'string' ? { text: o } : o;\n return request<boolean>('ui.confirm', params);\n },\n close() {\n notify('ui.close');\n },\n copy(text) {\n return request<void>('clipboard.write', { text: String(text) });\n },\n call<T>(method: string, params?: unknown) {\n return request<T>('engine.call', { method, params });\n },\n submit(action, params) {\n return request<{ op: unknown }>('job.submit', { action, params });\n },\n state: {\n get<T>(key: string) {\n return request<T | undefined>('state.get', { key });\n },\n set(key: string, value: unknown) {\n return request<void>('state.set', { key, value });\n },\n },\n license: {\n get() {\n return request<LicenseInfo>('license.get');\n },\n },\n on(event, handler) {\n let set = listeners.get(event);\n if (!set) listeners.set(event, (set = new Set()));\n set.add(handler);\n return () => set!.delete(handler);\n },\n onSave(handler) {\n saveHandler = handler;\n return () => {\n if (saveHandler === handler) saveHandler = null;\n };\n },\n request,\n };\n\n if (opts.saveShortcut !== false && typeof document !== 'undefined') {\n // ⚠ The keystroke is handled HERE and never forwarded: a frame that could\n // send key events to filex could press filex's own shortcuts. Ctrl+S runs\n // the app's own save handler, which saves through the bridge like any call.\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 's' || e.key === 'S') && saveHandler) {\n e.preventDefault();\n runSave().catch((err) => app.toast(String((err as Error)?.message ?? err), 'error'));\n }\n },\n true,\n );\n }\n return app;\n}\n\n/** Paint filex's look onto the interface's own document. */\nfunction applyLook(theme?: Theme, locale?: string, dir?: 'ltr' | 'rtl') {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (theme) {\n root.dataset.theme = theme.mode;\n root.style.colorScheme = theme.mode;\n for (const [k, v] of Object.entries(theme.tokens ?? {})) {\n if (/^--fe-[a-z0-9-]+$/.test(k) && typeof v === 'string') root.style.setProperty(k, v);\n }\n }\n if (locale) root.lang = locale;\n if (dir) root.dir = dir;\n}\n"],"names":["HELLO","PORT","METHODS","LIMITS","isHello","d","isPortMessage","isRequest","isKnownMethod","m","FilexError","err","connecting","connect","opts","open","resolve","reject","parent","expected","ancestorOrigin","timer","onMessage","ev","port","app","bridge","session","applyLook","t","l","v","list","seq","pending","listeners","saveHandler","p","answerHost","h","e","res","runSave","data","request","method","params","transfer","id","notify","payload","copy","buf","index","info","read","as","key","r","mime","o","name","pdf","from","kind","body","op","changesSeen","cursor","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filex-app-ui.iife.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get'\n /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */\n | 'ui.print'\n /**\n * Editing together (`coedit.*`, task #189): the session's log the host\n * seals, orders through filex's relay and opens again. Defined in filex\n * 0.55, which answers each `unavailable`; offered from 0.56 to an app\n * granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.\n */\n | 'coedit.join'\n | 'coedit.subscribe'\n | 'coedit.append'\n | 'coedit.lease'\n | 'coedit.cursor'\n | 'coedit.blob.put'\n | 'coedit.blob.get'\n | 'coedit.leave';\n\nexport const METHODS: readonly Method[] = [\n 'session.get',\n 'file.read',\n 'file.save',\n 'file.saveAs',\n 'ui.dirty',\n 'ui.title',\n 'ui.toast',\n 'ui.confirm',\n 'ui.close',\n 'clipboard.write',\n 'ui.download',\n 'engine.call',\n 'job.submit',\n 'state.get',\n 'state.set',\n 'license.get',\n 'ui.print',\n 'coedit.join',\n 'coedit.subscribe',\n 'coedit.append',\n 'coedit.lease',\n 'coedit.cursor',\n 'coedit.blob.put',\n 'coedit.blob.get',\n 'coedit.leave',\n];\n\n/** What the host tells the app, unasked. */\nexport type HostEvent =\n /** The interface's colours changed (light/dark, palette). `data: Theme`. */\n | 'theme'\n /** The reader's language changed. `data: {locale, dir}`. */\n | 'locale'\n /** The open file changed underneath (another tab, another person). */\n | 'file.changed'\n /** The person closed the frame's surroundings; last chance to say so. */\n | 'close.request'\n /** An administrator installed a new version of this app: reload to use it. */\n | 'app.updated'\n /** Editing together: the next entry of the session's log, opened. `data: CoEditEntry`. */\n | 'coedit.entry'\n /** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */\n | 'coedit.cursor'\n /**\n * Editing together: the host stopped handing entries over. `data:\n * CoEditDropped` - `gone`: the membership ended (join again); `broken`: an\n * entry did not check out (stop editing together). A connection that drops\n * for a moment is the host's own: it reconnects and says nothing.\n */\n | 'coedit.dropped';\n\n/** What the host may ask the app (and wait for). */\nexport type HostRequest =\n /** Save now (the host's Save button, a draft's \"Save to disk\"): the app\n * hands its document to `file.save` and answers when it is written. */\n 'save';\n\n/** Why a call was refused. */\nexport type ErrorCode =\n /** The app was not granted what the call needs (files:write for a save…). */\n | 'not_granted'\n /** No such file (an index past the opened files), no such key. */\n | 'not_found'\n /** The file cannot be written: a read-only storage, a view-only opening. */\n | 'read_only'\n /** The call's parameters are wrong. */\n | 'invalid'\n /** Over a limit (LIMITS). */\n | 'too_large'\n /** The server could not be reached or refused; `message` says what. */\n | 'failed'\n /** The person said no (a confirm, a save-as picker closed). */\n | 'cancelled'\n /** The host does not offer this here (no engine, no save handler). */\n | 'unavailable'\n | 'unknown_method';\n\nexport interface BridgeError {\n code: ErrorCode;\n message?: string;\n}\n\n/* ── messages ──────────────────────────────────────────────────────────── */\n\nexport interface HelloMessage {\n type: typeof HELLO;\n v: number;\n}\n\nexport interface PortMessage {\n type: typeof PORT;\n v: number;\n}\n\nexport interface RequestMessage {\n id: number;\n method: Method;\n params?: unknown;\n}\n\nexport interface ResponseMessage {\n id: number;\n result?: unknown;\n error?: BridgeError;\n}\n\nexport interface EventMessage {\n event: HostEvent;\n data?: unknown;\n}\n\nexport interface HostRequestMessage {\n hid: number;\n request: HostRequest;\n params?: unknown;\n}\n\nexport interface HostResponseMessage {\n hid: number;\n result?: unknown;\n error?: BridgeError;\n}\n\n/* ── shapes ────────────────────────────────────────────────────────────── */\n\n/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */\nexport interface Theme {\n mode: 'light' | 'dark';\n /** `--fe-bg`, `--fe-text`, `--fe-accent`… — the SDK sets them on `<html>`. */\n tokens: Record<string, string>;\n}\n\n/** One file the interface was opened with. Never a storage path. */\nexport interface FileInfo {\n index: number;\n name: string;\n /** Lower-case, no dot. */\n ext: string;\n size: number;\n mime: string;\n /** It cannot be saved over: read-only storage, view-only opening, or the\n * app holds no `files:write`. */\n readOnly: boolean;\n /**\n * 0.55.0: the file is end-to-end encrypted - `folder` in an encrypted\n * folder, `vault` in a vault (where only one person edits at a time),\n * `file` for a single encrypted file (`.fxe`). As the server says it;\n * ABSENT for a file that is not encrypted.\n *\n * ⚠ The server holds such a file only as ciphertext and has no key. Unless\n * `plaintext` is true, filex hands the app nothing of it: `file.read` is\n * refused (`failed` with `encrypted`), a save is refused, and `readOnly`\n * is true.\n */\n encrypted?: 'folder' | 'vault' | 'file';\n /**\n * 0.56.0: filex hands this encrypted file to the app in the clear. The\n * person's browser, which holds the folder key while the folder is\n * unlocked, decrypts it for `file.read` and encrypts what `file.save`\n * hands over before it leaves the browser; the server sees only\n * ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app\n * whose manifest says `encrypted_folders: {\"open\": true}` (the grant\n * `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.\n *\n * In a vault (`encrypted: 'vault'`) the browser reads the file from the\n * vault's packs and writes a save as the vault's next generation, under\n * the vault's write lock (one writer at a time). A save is refused -\n * `failed` with `vault_locked` - while somebody else writes the vault, and\n * with `vault_lock_lost` when this session's write lock ended before the\n * save was committed: nothing was written, save again. The vault locked\n * meanwhile (its keys left the browser): `unavailable`, and filex closes\n * the app.\n *\n * What changes for the app: a read and a save are held whole in the page\n * (at most LIMITS.maxEncryptedBytes); a save is written only over the file\n * as it was opened (`failed` with `changed` when it changed meanwhile);\n * `file.saveAs` answers `unavailable` (a copy elsewhere would leave the\n * folder decrypted); the app's module (`engine.call`, `job.submit`) is\n * never handed the file; `state.get` / `state.set` use a store of their\n * own for this folder, sealed in the person's browser (the server keeps it\n * and cannot read it; apart from the app's ordinary store, a little smaller\n * as it is measured sealed). Everything the app holds is plaintext: keep it\n * in the frame. In a folder whose names are encrypted too, `name` and\n * `ext` are the decrypted name's - the server never sees them.\n */\n plaintext?: true;\n}\n\n/** What `session.get` answers — what the app knows about where it runs. */\nexport interface Session {\n v: number;\n app: { name: string; version: string };\n view: { id: string; placement: 'modal' | 'page' | 'inspector' | 'home' | 'viewer' };\n /** The reader's language tag (`tr`, `pt-br`) and its direction. */\n locale: string;\n dir: 'ltr' | 'rtl';\n theme: Theme;\n /**\n * The person's display name (their profile's, else their username); never\n * their e-mail, never a token. '' where nobody is signed in (a public\n * page). An editor writes it as the author of comments and tracked\n * changes.\n */\n user: { name: string };\n /**\n * filex 0.57: the language the administrator fixed for document editors\n * (Admin → External services → ONLYOFFICE → Editor language,\n * `FILEX_ONLYOFFICE_LANG`; a code such as `de`, `pt-PT`, `zh-TW`). An app\n * that draws a document editor draws it in this language rather than the\n * reader's `locale`; its own words stay in `locale`. Absent: each person's\n * own language decides.\n */\n editor_lang?: string;\n files: FileInfo[];\n /** The permissions this app was granted — so it can hide what it may not do. */\n grants: string[];\n /** The administrator's non-secret settings for this app (grant `settings`). */\n settings?: Record<string, string>;\n}\n\n/** `file.read` params. */\nexport interface ReadParams {\n index?: number;\n /** `stream` (a transferred ReadableStream<Uint8Array>), `bytes` (an\n * ArrayBuffer), or `text` (UTF-8, at most LIMITS.maxTextBytes). */\n as?: 'stream' | 'bytes' | 'text';\n}\n\n/**\n * How a CSV file is written (filex 0.57): `ReadResult.text_dialect` of a\n * `.csv`, read by the server from the file's own bytes.\n */\nexport interface TextDialect {\n /** One character: `,`, `;`, `\\t` or `|`. */\n delimiter: string;\n /** The file starts with a byte order mark. */\n bom: boolean;\n /** Its lines end in CRLF (else LF). */\n crlf: boolean;\n /**\n * Its encoding as a browser's TextDecoder names it: `utf-8`, `utf-16le`,\n * `utf-16be`, or a code page (`windows-1250`, `windows-1251`,\n * `windows-1252`, `windows-1254`). `file.read` with `as: 'text'` decodes\n * with it; `bytes` and `stream` hand the bytes as they are.\n */\n encoding: string;\n}\n\nexport interface ReadResult {\n name: string;\n size: number;\n mime: string;\n stream?: ReadableStream<Uint8Array>;\n bytes?: ArrayBuffer;\n text?: string;\n /**\n * filex 0.57: how a `.csv` is written (its delimiter, byte order mark,\n * line ends, encoding). Absent for any other file, for a file of an\n * encrypted folder (the server holds its ciphertext only), and from filex\n * 0.56 and older.\n */\n text_dialect?: TextDialect;\n}\n\n/**\n * What an app says about the CSV it saves (filex 0.57; `SaveParams.csv`):\n * how ITS bytes are written. filex reads the same from the bytes and refuses\n * a save they contradict (`invalid`, message `csv_dialect`): nothing is\n * written. Whatever the app wrote, the file is written in its own way - its\n * delimiter, byte order mark, line ends and encoding - with every cell nobody\n * changed as the file has it. A field left out is not checked; no `csv` at\n * all is read from the bytes alone.\n */\nexport interface CsvDeclaration {\n /** One character: `,`, `;`, `\\t` or `|`. */\n delimiter?: string;\n /** As TextDialect.encoding names it (`utf-8` for a string the SDK sent). */\n encoding?: string;\n /** The bytes start with a byte order mark. */\n bom?: boolean;\n /** Their line ends. */\n eol?: 'crlf' | 'lf';\n}\n\n/** `file.save` params: the new content of the opened file. */\nexport interface SaveParams {\n index?: number;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n /**\n * 0.55.0, editing together: the last log entry (`CoEditEntry.seq`) the\n * saved document holds, so the session knows what is saved. Ignored\n * outside a session.\n */\n through?: number;\n /**\n * filex 0.57: what the app says about the CSV it saves over a `.csv`\n * (CsvDeclaration). Ignored for any other file and for a file of an\n * encrypted folder; filex 0.56 and older ignore it.\n */\n csv?: CsvDeclaration;\n /**\n * filex 0.57: the save is in this format (an extension: `docx`, `xlsx`,\n * `pptx`, `docm`, `xlsm`, `pptm`, `odt`, `ods`, `odp`), which the opened\n * file cannot hold (a `.doc`, `.xls`, `.ppt`...). It is written BESIDE the\n * file, which is left as it is, under a name filex chooses - the file's own\n * name with that extension, `rapor (2).docx` when that is taken - and only\n * for a person who may create files in that folder (`permission_denied`\n * otherwise). `SaveResult.name` says where; later saves of the same\n * opening go to that file. The same extension as the file's own: an\n * ordinary save over it. Refused (`unavailable`) for a file of an\n * encrypted folder. filex 0.56 and older ignore it and write over the file.\n */\n beside?: string;\n /**\n * filex 0.57: `'copy'` - when the file changed since the app opened it\n * (somebody else saved it, a sync client wrote it), the save is written as\n * a conflict copy beside it (`<name>.filex-conflict-<UTC time>.<ext>`)\n * instead of being refused, and `SaveResult.conflict` names it; later saves\n * of the same opening go to that copy. Without it such a save is refused\n * (`failed`, message `changed`), as in 0.56. A file of an encrypted folder\n * is never copied: refused as before.\n */\n conflict?: 'copy';\n}\n\nexport interface SaveResult {\n saved: true;\n size: number;\n /**\n * filex 0.57: the name of the file the save was written to when it is not\n * the opened file - the one beside it (`beside`) or the conflict copy\n * (`conflict`). Absent for a save over the opened file.\n */\n name?: string;\n /** filex 0.57: the save went beside the file, in the format `beside` named. */\n beside?: true;\n /** filex 0.57: the save went to this conflict copy (`conflict: 'copy'`). */\n conflict?: string;\n}\n\n/**\n * `ui.download`: a file for the person's own disk. filex does it (a sandboxed\n * frame cannot download), with the app's `ui:download` grant, on a gesture in\n * the frame or the person's yes, never over LIMITS.maxDownloadBytes.\n */\nexport interface DownloadParams {\n /** The file name offered — a name, not a path. */\n name: string;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface DownloadResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.print` (filex 0.55.0): a PDF for the browser's print dialog. A\n * sandboxed frame may not open that dialog (measured: Chromium ignores\n * `print()` there and says so, Firefox ignores it), so filex prints the PDF\n * from a page of its own, with the app's `ui:print` grant, never over\n * LIMITS.maxPrintBytes. filex asks every time, above the frame, and the\n * dialog opens only on the person's click on filex's Allow. The print dialog\n * can save the PDF too, so `ui:print` is the same kind of grant as\n * `ui:download`. filex 0.54 and older answer `unknown_method`.\n */\nexport interface PrintParams {\n /** The document's name, for the question filex may ask - a name, not a path. */\n name: string;\n /** The PDF: a transferred ArrayBuffer or ReadableStream<Uint8Array>. */\n data: ReadableStream<Uint8Array> | ArrayBuffer;\n /** `application/pdf` when given; nothing else is printed. */\n mime?: string;\n}\n\nexport interface PrintResult {\n /** The browser's print dialog was opened with the PDF. */\n printed: true;\n size: number;\n}\n\n/* ── editing together (coedit.*: filex 0.55 defined, 0.56 offers) ──────── */\n\n/** What a member appends: the editor's changes, a lock request, a release. */\nexport type CoEditKind = 'changes' | 'lock' | 'release';\n\n/** A member of the session. */\nexport interface CoEditMember {\n /** The relay's id for the connection. */\n client: string;\n /** An opaque id of the person, the same for each of their connections in\n * this session; never an account id. */\n user: string;\n /** The person's display name. */\n name: string;\n /** The editor's per-session user index: given once, never twice. */\n indexUser: number;\n /** May append changes (an editor role and the right to change the file). */\n canEdit: boolean;\n}\n\n/** `coedit.join` params: join (or start) the session of an opened file. */\nexport interface CoEditJoinParams {\n index?: number;\n}\n\n/**\n * What `coedit.join` answers: who the app is in the session and how far\n * the log has gone. The entries come as `coedit.entry` events once the app\n * subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`\n * when filex offers no editing together here (filex 0.55, editing together\n * turned off, an encrypted file, a vault): the app edits alone - the message\n * says why (`not_ready` after waiting for a session's creator, `encrypted`,\n * `nothing_to_watch` for a person who may only read the file while nobody\n * edits it, or the server's code: `coedit_off`, `coedit_build`,\n * `coedit_file_changed`).\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /**\n * The last entry's seq when filex handed the session over (0: an empty\n * log). filex 0.56: the changes this browser kept from an earlier opening\n * of the file (a lost connection, a closed tab), which filex sends before\n * it answers, are in the log up to here - an editor that starts once it\n * has read this far opens with them.\n */\n head: number;\n /** The last `changes` entry's seq, as far as `head`. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\n /**\n * filex 0.56: this member started the session. Before anybody else can\n * join it puts the base document - EXACTLY the bytes it opened - with\n * `coedit.blob.put('base', bytes)`. A member that did not start it opens\n * `coedit.blob.get('base')`, never the file: the file may hold a save of\n * the session already, and the log applies to the base.\n */\n created: boolean;\n}\n\n/** `coedit.dropped` event data (filex 0.56). */\nexport interface CoEditDropped {\n /** The last entry the host handed over. */\n from: number;\n /** `gone`: the membership ended (the session closed, or the page was away\n * too long) - join again. `broken`: an entry did not check out (missing,\n * moved, altered, replayed) - stop editing together. */\n reason: 'gone' | 'broken';\n}\n\n/** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */\nexport interface CoEditSubscribeParams {\n from: number;\n}\n\n/** `coedit.append` params: the host seals it and the relay places it. */\nexport interface CoEditAppendParams {\n kind: CoEditKind;\n body: unknown;\n}\n\nexport interface CoEditAppendResult {\n /** Where it landed; it comes back as a `coedit.entry` too. */\n seq: number;\n}\n\n/** `coedit.lease`: the changes lease (one writer at a time, one that has\n * seen every change). */\nexport interface CoEditLeaseParams {\n op: 'acquire' | 'release';\n /** The seq of the last `changes` entry the app applied. */\n changesSeen: number;\n}\n\nexport interface CoEditLeaseResult {\n granted: boolean;\n}\n\n/** `coedit.cursor` params: passed to the other members, not kept. The host\n * sends a few a second at most (the last one wins). */\nexport interface CoEditCursorParams {\n cursor: unknown;\n}\n\n/** `coedit.blob.put` / `coedit.blob.get`: the session's sealed blobs (the\n * base document, images added while editing). A name, not a path. */\nexport interface CoEditBlobPutParams {\n name: string;\n data: ArrayBuffer;\n}\n\nexport interface CoEditBlobGetParams {\n name: string;\n}\n\nexport interface CoEditBlobGetResult {\n name: string;\n bytes: ArrayBuffer;\n}\n\ninterface CoEditEntryBase {\n seq: number;\n /** The relay's time, Unix ms - the same for every member. */\n at: number;\n client: string;\n}\n\n/** One entry of the session's log, opened and checked by filex. */\nexport type CoEditEntry =\n | (CoEditEntryBase & { kind: CoEditKind; body: unknown })\n | (CoEditEntryBase & { kind: 'join'; member: CoEditMember })\n | (CoEditEntryBase & { kind: 'leave' })\n | (CoEditEntryBase & { kind: 'saved'; through: number });\n\n/** `coedit.cursor` event data. */\nexport interface CoEditCursor {\n client: string;\n cursor: unknown;\n}\n\n/**\n * `file.saveAs`: a NEW file, in a folder the person picks in filex's own\n * folder dialog (the one its Move to… uses), which opens in the opened\n * file's folder. Closing the dialog answers `cancelled`.\n */\nexport interface SaveAsParams {\n /** The file name — a name, not a path. */\n name: string;\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveAsResult {\n saved: true;\n name: string;\n size: number;\n}\n\nexport interface ToastParams {\n text: string;\n tone?: 'info' | 'success' | 'warning' | 'error';\n}\n\nexport interface ConfirmParams {\n title?: string;\n text: string;\n confirm?: string;\n cancel?: string;\n danger?: boolean;\n}\n\n/**\n * `license.get` (filex 0.52.0): what the app's license says. A paid app is\n * installed from a store, which issues its license and is asked about it every\n * day; filex keeps the key and never hands it to the app. `status`:\n *\n * - `free`: not a paid app;\n * - `valid`: the store said valid at its last check;\n * - `grace`: valid, but the store could not be asked since - it holds until\n * the grace the store signed ends;\n * - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,\n * `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held\n * and its interface is not served, so an app rarely reads one of these.\n *\n * `valid_until` / `updates_until` are there for a valid license (dates as\n * RFC 3339). Who holds the license is not the app's to read. filex 0.51.0\n * and older answer `unknown_method`.\n */\nexport interface LicenseInfo {\n status: string;\n valid_until?: string;\n updates_until?: string;\n}\n\n/** `engine.call`: the app's own module, `ui_call` export. */\nexport interface EngineCallParams {\n method: string;\n params?: unknown;\n}\n\n/** `job.submit`: queue one of the app's actions on the opened files. */\nexport interface JobSubmitParams {\n action: string;\n params?: Record<string, unknown>;\n}\n\n/* ── limits ────────────────────────────────────────────────────────────── */\n\nexport const LIMITS = {\n /** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */\n maxTextBytes: 32 << 20,\n /** One `state.set` value, as JSON. */\n maxStateBytes: 8 << 10,\n /** A `ui.toast` / `ui.title` text, in characters. */\n maxLineChars: 300,\n /** How long the SDK waits for the host's port. */\n connectTimeoutMs: 10_000,\n /** The largest file `ui.download` hands the person (streamed or not). */\n maxDownloadBytes: 256 << 20,\n /**\n * The largest PDF `ui.print` prints (filex 0.55). Lower than a download:\n * a download can stream to disk, a print is held whole in the page (one\n * Blob) until the print dialog has read it, and a document worth printing\n * is far smaller.\n */\n maxPrintBytes: 64 << 20,\n /**\n * The largest file of an encrypted folder filex hands an app in the clear\n * or takes back from it (filex 0.56, `FileInfo.plaintext`): it is\n * decrypted and encrypted whole in the page, as filex's own explorer does\n * for a file of this size.\n */\n maxEncryptedBytes: 200 << 20,\n} as const;\n\n/* ── guards ────────────────────────────────────────────────────────────── */\n\nexport function isHello(d: unknown): d is HelloMessage {\n return !!d && typeof d === 'object' && (d as HelloMessage).type === HELLO && typeof (d as HelloMessage).v === 'number';\n}\n\nexport function isPortMessage(d: unknown): d is PortMessage {\n return !!d && typeof d === 'object' && (d as PortMessage).type === PORT;\n}\n\nexport function isRequest(d: unknown): d is RequestMessage {\n return (\n !!d &&\n typeof d === 'object' &&\n typeof (d as RequestMessage).id === 'number' &&\n typeof (d as RequestMessage).method === 'string'\n );\n}\n\nexport function isKnownMethod(m: string): m is Method {\n return (METHODS as readonly string[]).includes(m);\n}\n","/**\n * @brftech/filex-app-ui — the SDK an app's own interface bundles.\n *\n * ```js\n * import { connect } from '@brftech/filex-app-ui';\n *\n * const fx = await connect(); // the handshake with filex\n * const file = await fx.open(); // the file the app was opened with\n * editor.setValue(await file.text());\n * editor.onChange(() => fx.dirty(true));\n * fx.onSave(async () => editor.getValue()); // filex's Save, a draft's \"Save to disk\", Ctrl+S\n * ```\n *\n * No dependency, a few KB. The interface runs in a sandboxed frame with no\n * network and no storage of its own: every call below goes to filex over the\n * MessagePort the handshake set up, and filex decides it (docs/APP-PLUGINS.md\n * → \"An app's own interface\"). The protocol itself is `./protocol`.\n */\nimport {\n BRIDGE_VERSION,\n HELLO,\n LIMITS,\n isPortMessage,\n type BridgeError,\n type ConfirmParams,\n type ErrorCode,\n type FileInfo,\n type HostEvent,\n type HostRequestMessage,\n type Method,\n type ReadResult,\n type SaveAsResult,\n type SaveResult,\n type Session,\n type Theme,\n type ToastParams,\n type DownloadResult,\n type LicenseInfo,\n type PrintResult,\n type CoEditHello,\n type CoEditKind,\n type CoEditAppendResult,\n type CoEditLeaseResult,\n type CoEditBlobGetResult,\n type CsvDeclaration,\n} from './protocol';\n\nexport * from './protocol';\n\n/** A call filex refused, with the reason as a code. */\nexport class FilexError extends Error {\n readonly code: ErrorCode;\n constructor(err: BridgeError) {\n super(err.message || err.code);\n this.name = 'FilexError';\n this.code = err.code;\n }\n}\n\n/** Content the app hands to a save. */\nexport type SaveData = string | Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>;\n\n/** One opened file, read the way the app needs it. */\nexport interface OpenedFile extends FileInfo {\n text(): Promise<string>;\n bytes(): Promise<ArrayBuffer>;\n stream(): Promise<ReadableStream<Uint8Array>>;\n /**\n * The whole answer of a read (filex 0.57): the content `as` asks for, and\n * what filex knows about how the file is written (`text_dialect`, for a\n * `.csv`).\n */\n read(as?: 'stream' | 'bytes' | 'text'): Promise<ReadResult>;\n /** Save new content over this file (a new version, or the draft it is). */\n save(data: SaveData, mime?: string): Promise<SaveResult>;\n}\n\nexport interface ConnectOptions {\n /** How long to wait for filex (default 10 s). */\n timeoutMs?: number;\n /**\n * Put filex's colours (`--fe-*`), `lang`, `dir` and `data-theme` on\n * `<html>`, and keep them current (default true).\n */\n applyTheme?: boolean;\n /** Ctrl/Cmd+S inside the interface runs the save handler (default true). */\n saveShortcut?: boolean;\n}\n\nexport interface FilexApp {\n readonly session: Session;\n /** The file at `index` (default the first) the interface was opened with. */\n open(index?: number): Promise<OpenedFile>;\n /**\n * Save new content over an opened file. `through` (filex 0.56, editing\n * together): the last log entry the saved document holds, so the session\n * knows what is saved. `csv` (filex 0.57): how the CSV saved over a `.csv`\n * is written (CsvDeclaration); filex checks it against the bytes and\n * writes the file in its own way. `beside` (filex 0.57): the save is in\n * this format, which the file cannot hold - written beside it under a name\n * filex chooses (`SaveResult.name`). `conflict: 'copy'` (filex 0.57): a\n * file that changed since it was opened gets the save as a conflict copy\n * (`SaveResult.conflict`) instead of a refusal.\n */\n save(\n data: SaveData,\n opts?: { index?: number; mime?: string; through?: number; csv?: CsvDeclaration; beside?: string; conflict?: 'copy' },\n ): Promise<SaveResult>;\n /** Save a NEW file; filex asks the person where (its own folder picker). */\n saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;\n /** Unsaved changes, or not: filex asks before the person leaves them. */\n dirty(on: boolean): void;\n title(text: string): void;\n toast(text: string, tone?: ToastParams['tone']): void;\n confirm(opts: ConfirmParams | string): Promise<boolean>;\n /** Ask filex to close the interface (it asks first when there are changes). */\n close(): void;\n /** Put text on the clipboard (filex does it, the same way in every browser). */\n copy(text: string): Promise<void>;\n /**\n * Hand the person a file for their own disk (the app needs `ui.download`\n * in its manifest). Call it from a click or a key press: without one,\n * filex asks the person first. `cancelled` when they say no.\n */\n download(name: string, data: SaveData, mime?: string): Promise<DownloadResult>;\n /**\n * Print a PDF (filex 0.55; the app needs `ui.print` in its manifest): a\n * sandboxed frame may not open the print dialog, so filex prints it from\n * its own page. filex asks the person every time, and the dialog opens on\n * their click on filex's Allow; at most LIMITS.maxPrintBytes. filex 0.54\n * and older answer `unknown_method`, and a host whose print page may not\n * be shown `unavailable`: hand the PDF over with `download` then.\n */\n print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;\n /**\n * Editing together (filex 0.56; the app's manifest says `\"co_edit\":\n * {\"open\": true}`, granted as `files:co-edit`). filex seals what the app appends, orders it\n * through its relay and hands every member the same entries in the same\n * order (`coedit.entry` events after `subscribe`); the app never holds a\n * key. `join` answering `unavailable` (filex 0.55, editing together off\n * here, an encrypted file) means: edit alone. The member that started the\n * session (`created`) puts the base document before anybody else may join.\n */\n coedit: {\n join(index?: number): Promise<CoEditHello>;\n subscribe(from: number): Promise<void>;\n append(kind: CoEditKind, body: unknown): Promise<CoEditAppendResult>;\n lease(op: 'acquire' | 'release', changesSeen: number): Promise<boolean>;\n cursor(cursor: unknown): void;\n putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView): Promise<void>;\n getBlob(name: string): Promise<ArrayBuffer>;\n leave(): Promise<void>;\n };\n /** Call the app's own module (`ui_call` export) with the opened files. */\n call<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Queue one of the app's actions on the opened files; answers the op. */\n submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;\n /**\n * This person's small store for this app (JSON values, 8 KiB each). While\n * the app holds a file of an encrypted folder in the clear\n * (`FileInfo.plaintext`, filex 0.56) it is another store, sealed in the\n * browser - apart from this one, and empty while the folder is locked.\n */\n state: {\n get<T = unknown>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n };\n /**\n * The app's license (filex 0.52.0; a paid app installed from a store):\n * `{status: \"free\"}` for a free app, `{status: \"valid\", valid_until,\n * updates_until}` for a licensed one. Never the key, never the licensee.\n */\n license: {\n get(): Promise<LicenseInfo>;\n };\n /** Listen to filex. Returns the unsubscribe. */\n on(event: HostEvent, handler: (data: unknown) => void): () => void;\n /**\n * What to save when filex asks (its Save button, a draft's \"Save to disk\",\n * Ctrl+S): return the document and the SDK saves it over the opened file;\n * return nothing if the handler saved by itself.\n */\n onSave(handler: () => SaveData | void | Promise<SaveData | void>): () => void;\n /** Raw call, for a method this SDK has no helper for. */\n request<T = unknown>(method: Method, params?: unknown, transfer?: Transferable[]): Promise<T>;\n}\n\nlet connecting: Promise<FilexApp> | null = null;\n\n/**\n * The handshake. Resolves once filex answered with its port and the session;\n * rejects outside filex or when filex did not answer in time. Calling it again\n * answers the same connection.\n */\nexport function connect(opts: ConnectOptions = {}): Promise<FilexApp> {\n if (!connecting) connecting = open(opts);\n return connecting;\n}\n\nfunction open(opts: ConnectOptions): Promise<FilexApp> {\n return new Promise<FilexApp>((resolve, reject) => {\n if (typeof window === 'undefined' || window.parent === window) {\n reject(new FilexError({ code: 'unavailable', message: 'not running inside filex' }));\n return;\n }\n const parent = window.parent;\n // Where filex runs, when the browser says so (Chrome, Safari). A frame\n // drawn with referrerpolicy=\"no-referrer\" has no document.referrer, and\n // Firefox has no ancestorOrigins: then `source === parent` is the check,\n // which is the one that matters (only the parent window IS the parent).\n const expected = ancestorOrigin();\n const timer = setTimeout(() => {\n window.removeEventListener('message', onMessage);\n reject(new FilexError({ code: 'unavailable', message: 'filex did not answer' }));\n }, opts.timeoutMs ?? LIMITS.connectTimeoutMs);\n\n function onMessage(ev: MessageEvent) {\n if (ev.source !== parent || !isPortMessage(ev.data) || !ev.ports[0]) return;\n if (expected && ev.origin !== expected) return;\n window.removeEventListener('message', onMessage);\n clearTimeout(timer);\n const port = ev.ports[0];\n const app = bridge(port, opts);\n app\n .request<Session>('session.get')\n .then((session) => {\n (app as { session: Session }).session = session;\n if (opts.applyTheme !== false) {\n applyLook(session.theme, session.locale, session.dir);\n app.on('theme', (t) => applyLook(t as Theme));\n app.on('locale', (l) => {\n const v = l as { locale?: string; dir?: 'ltr' | 'rtl' };\n applyLook(undefined, v.locale, v.dir);\n });\n }\n resolve(app);\n })\n .catch(reject);\n }\n window.addEventListener('message', onMessage);\n parent.postMessage({ type: HELLO, v: BRIDGE_VERSION }, '*');\n });\n}\n\nfunction ancestorOrigin(): string {\n try {\n const list = (window.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins;\n if (list && list.length > 0 && list[0] !== 'null') return list[0];\n } catch {\n /* no such API */\n }\n return '';\n}\n\nfunction bridge(port: MessagePort, opts: ConnectOptions): FilexApp {\n let seq = 0;\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();\n const listeners = new Map<HostEvent, Set<(data: unknown) => void>>();\n let saveHandler: (() => SaveData | void | Promise<SaveData | void>) | null = null;\n\n port.onmessage = (ev: MessageEvent) => {\n const m = ev.data as Record<string, unknown> | null;\n if (!m || typeof m !== 'object') return;\n if (typeof m.id === 'number') {\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n if (m.error) p.reject(new FilexError(m.error as BridgeError));\n else p.resolve(m.result);\n return;\n }\n if (typeof m.hid === 'number') {\n void answerHost(m as unknown as HostRequestMessage);\n return;\n }\n if (typeof m.event === 'string') {\n for (const h of listeners.get(m.event as HostEvent) ?? []) {\n try {\n h(m.data);\n } catch (e) {\n console.error('[filex-app-ui] listener failed', e);\n }\n }\n }\n };\n\n async function answerHost(m: HostRequestMessage) {\n if (m.request === 'save') {\n if (!saveHandler) {\n port.postMessage({ hid: m.hid, error: { code: 'unavailable', message: 'this app has no save handler' } });\n return;\n }\n try {\n const res = await runSave();\n port.postMessage({ hid: m.hid, result: res ?? { saved: true } });\n } catch (e) {\n const err = e instanceof FilexError ? { code: e.code, message: e.message } : { code: 'failed', message: String((e as Error)?.message ?? e) };\n port.postMessage({ hid: m.hid, error: err });\n }\n return;\n }\n port.postMessage({ hid: m.hid, error: { code: 'unknown_method' } });\n }\n\n async function runSave(): Promise<SaveResult | null> {\n if (!saveHandler) return null;\n const data = await saveHandler();\n if (data === undefined || data === null) return null;\n return app.save(data as SaveData);\n }\n\n function request<T>(method: Method, params?: unknown, transfer: Transferable[] = []): Promise<T> {\n const id = ++seq;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject });\n try {\n port.postMessage({ id, method, params }, transfer);\n } catch (e) {\n pending.delete(id);\n reject(e);\n }\n });\n }\n\n function notify(method: Method, params?: unknown) {\n request(method, params).catch((e) => console.warn('[filex-app-ui]', method, e));\n }\n\n async function payload(data: SaveData): Promise<{ data: ReadableStream<Uint8Array> | ArrayBuffer | string; transfer: Transferable[] }> {\n if (typeof data === 'string') return { data, transfer: [] };\n if (data instanceof ArrayBuffer) return { data, transfer: [data] };\n if (ArrayBuffer.isView(data)) {\n const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n return { data: copy, transfer: [copy] };\n }\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n const buf = await data.arrayBuffer();\n return { data: buf, transfer: [buf] };\n }\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {\n return { data, transfer: [data as unknown as Transferable] };\n }\n throw new FilexError({ code: 'invalid', message: 'save takes a string, a Blob, bytes or a ReadableStream' });\n }\n\n const app: FilexApp = {\n session: undefined as unknown as Session,\n async open(index = 0) {\n const info = app.session?.files?.[index];\n if (!info) throw new FilexError({ code: 'not_found', message: `no file at ${index}` });\n const read = <K extends keyof ReadResult>(as: 'stream' | 'bytes' | 'text', key: K) =>\n request<ReadResult>('file.read', { index, as }).then((r) => r[key] as NonNullable<ReadResult[K]>);\n return {\n ...info,\n text: () => read('text', 'text'),\n bytes: () => read('bytes', 'bytes'),\n stream: () => read('stream', 'stream'),\n read: (as: 'stream' | 'bytes' | 'text' = 'bytes') => request<ReadResult>('file.read', { index, as }),\n save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),\n };\n },\n async save(data, o = {}) {\n const p = await payload(data);\n const params: Record<string, unknown> = { index: o.index ?? 0, data: p.data, mime: o.mime };\n if (typeof o.through === 'number') params.through = o.through;\n if (o.csv) params.csv = { ...o.csv };\n if (o.beside) params.beside = o.beside;\n if (o.conflict) params.conflict = o.conflict;\n return request<SaveResult>('file.save', params, p.transfer);\n },\n async saveAs(name, data, mime) {\n const p = await payload(data);\n return request<SaveAsResult>('file.saveAs', { name, data: p.data, mime }, p.transfer);\n },\n async download(name, data, mime) {\n const p = await payload(data);\n return request<DownloadResult>('ui.download', { name, data: p.data, mime }, p.transfer);\n },\n async print(name, pdf) {\n const p = await payload(pdf);\n return request<PrintResult>('ui.print', { name, data: p.data, mime: 'application/pdf' }, p.transfer);\n },\n coedit: {\n join(index = 0) {\n return request<CoEditHello>('coedit.join', { index });\n },\n subscribe(from: number) {\n return request<void>('coedit.subscribe', { from });\n },\n append(kind: CoEditKind, body: unknown) {\n return request<CoEditAppendResult>('coedit.append', { kind, body });\n },\n async lease(op: 'acquire' | 'release', changesSeen: number) {\n const r = await request<CoEditLeaseResult>('coedit.lease', { op, changesSeen });\n return !!r?.granted;\n },\n cursor(cursor: unknown) {\n notify('coedit.cursor', { cursor });\n },\n async putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView) {\n const p = await payload(data);\n if (!(p.data instanceof ArrayBuffer)) throw new FilexError({ code: 'invalid', message: 'a blob is bytes' });\n await request<void>('coedit.blob.put', { name, data: p.data }, p.transfer);\n },\n async getBlob(name: string) {\n const r = await request<CoEditBlobGetResult>('coedit.blob.get', { name });\n return r.bytes;\n },\n async leave() {\n await request<void>('coedit.leave');\n },\n },\n dirty(on) {\n notify('ui.dirty', { dirty: !!on });\n },\n title(text) {\n notify('ui.title', { text: String(text).slice(0, LIMITS.maxLineChars) });\n },\n toast(text, tone) {\n notify('ui.toast', { text: String(text).slice(0, LIMITS.maxLineChars), tone });\n },\n confirm(o) {\n const params = typeof o === 'string' ? { text: o } : o;\n return request<boolean>('ui.confirm', params);\n },\n close() {\n notify('ui.close');\n },\n copy(text) {\n return request<void>('clipboard.write', { text: String(text) });\n },\n call<T>(method: string, params?: unknown) {\n return request<T>('engine.call', { method, params });\n },\n submit(action, params) {\n return request<{ op: unknown }>('job.submit', { action, params });\n },\n state: {\n get<T>(key: string) {\n return request<T | undefined>('state.get', { key });\n },\n set(key: string, value: unknown) {\n return request<void>('state.set', { key, value });\n },\n },\n license: {\n get() {\n return request<LicenseInfo>('license.get');\n },\n },\n on(event, handler) {\n let set = listeners.get(event);\n if (!set) listeners.set(event, (set = new Set()));\n set.add(handler);\n return () => set!.delete(handler);\n },\n onSave(handler) {\n saveHandler = handler;\n return () => {\n if (saveHandler === handler) saveHandler = null;\n };\n },\n request,\n };\n\n if (opts.saveShortcut !== false && typeof document !== 'undefined') {\n // ⚠ The keystroke is handled HERE and never forwarded: a frame that could\n // send key events to filex could press filex's own shortcuts. Ctrl+S runs\n // the app's own save handler, which saves through the bridge like any call.\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 's' || e.key === 'S') && saveHandler) {\n e.preventDefault();\n runSave().catch((err) => app.toast(String((err as Error)?.message ?? err), 'error'));\n }\n },\n true,\n );\n }\n return app;\n}\n\n/** Paint filex's look onto the interface's own document. */\nfunction applyLook(theme?: Theme, locale?: string, dir?: 'ltr' | 'rtl') {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (theme) {\n root.dataset.theme = theme.mode;\n root.style.colorScheme = theme.mode;\n for (const [k, v] of Object.entries(theme.tokens ?? {})) {\n if (/^--fe-[a-z0-9-]+$/.test(k) && typeof v === 'string') root.style.setProperty(k, v);\n }\n }\n if (locale) root.lang = locale;\n if (dir) root.dir = dir;\n}\n"],"names":["HELLO","PORT","METHODS","LIMITS","isHello","d","isPortMessage","isRequest","isKnownMethod","m","FilexError","err","connecting","connect","opts","open","resolve","reject","parent","expected","ancestorOrigin","timer","onMessage","ev","port","app","bridge","session","applyLook","t","l","v","list","seq","pending","listeners","saveHandler","p","answerHost","h","e","res","runSave","data","request","method","params","transfer","id","notify","payload","copy","buf","index","info","read","as","key","r","mime","o","name","pdf","from","kind","body","op","changesSeen","cursor","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
package/dist/filex-app-ui.js
CHANGED
|
@@ -83,7 +83,7 @@ function O(n) {
|
|
|
83
83
|
function g(y) {
|
|
84
84
|
if (y.source !== o || !L(y.data) || !y.ports[0] || l && y.origin !== l) return;
|
|
85
85
|
window.removeEventListener("message", g), clearTimeout(a);
|
|
86
|
-
const
|
|
86
|
+
const r = y.ports[0], c = T(r, n);
|
|
87
87
|
c.request("session.get").then((f) => {
|
|
88
88
|
c.session = f, n.applyTheme !== !1 && (v(f.theme, f.locale, f.dir), c.on("theme", (d) => v(d)), c.on("locale", (d) => {
|
|
89
89
|
const e = d;
|
|
@@ -110,9 +110,9 @@ function T(n, u) {
|
|
|
110
110
|
const t = e.data;
|
|
111
111
|
if (!(!t || typeof t != "object")) {
|
|
112
112
|
if (typeof t.id == "number") {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
o.delete(t.id), t.error ?
|
|
113
|
+
const s = o.get(t.id);
|
|
114
|
+
if (!s) return;
|
|
115
|
+
o.delete(t.id), t.error ? s.reject(new m(t.error)) : s.resolve(t.result);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
if (typeof t.hid == "number") {
|
|
@@ -120,9 +120,9 @@ function T(n, u) {
|
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
if (typeof t.event == "string")
|
|
123
|
-
for (const
|
|
123
|
+
for (const s of l.get(t.event) ?? [])
|
|
124
124
|
try {
|
|
125
|
-
|
|
125
|
+
s(t.data);
|
|
126
126
|
} catch (i) {
|
|
127
127
|
console.error("[filex-app-ui] listener failed", i);
|
|
128
128
|
}
|
|
@@ -138,8 +138,8 @@ function T(n, u) {
|
|
|
138
138
|
const t = await y();
|
|
139
139
|
n.postMessage({ hid: e.hid, result: t ?? { saved: !0 } });
|
|
140
140
|
} catch (t) {
|
|
141
|
-
const
|
|
142
|
-
n.postMessage({ hid: e.hid, error:
|
|
141
|
+
const s = t instanceof m ? { code: t.code, message: t.message } : { code: "failed", message: String(t?.message ?? t) };
|
|
142
|
+
n.postMessage({ hid: e.hid, error: s });
|
|
143
143
|
}
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
@@ -150,19 +150,19 @@ function T(n, u) {
|
|
|
150
150
|
const e = await a();
|
|
151
151
|
return e == null ? null : d.save(e);
|
|
152
152
|
}
|
|
153
|
-
function
|
|
153
|
+
function r(e, t, s = []) {
|
|
154
154
|
const i = ++p;
|
|
155
155
|
return new Promise((b, w) => {
|
|
156
156
|
o.set(i, { resolve: b, reject: w });
|
|
157
157
|
try {
|
|
158
|
-
n.postMessage({ id: i, method: e, params: t },
|
|
158
|
+
n.postMessage({ id: i, method: e, params: t }, s);
|
|
159
159
|
} catch (B) {
|
|
160
160
|
o.delete(i), w(B);
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
function c(e, t) {
|
|
165
|
-
|
|
165
|
+
r(e, t).catch((s) => console.warn("[filex-app-ui]", e, s));
|
|
166
166
|
}
|
|
167
167
|
async function f(e) {
|
|
168
168
|
if (typeof e == "string") return { data: e, transfer: [] };
|
|
@@ -184,57 +184,58 @@ function T(n, u) {
|
|
|
184
184
|
async open(e = 0) {
|
|
185
185
|
const t = d.session?.files?.[e];
|
|
186
186
|
if (!t) throw new m({ code: "not_found", message: `no file at ${e}` });
|
|
187
|
-
const
|
|
187
|
+
const s = (i, b) => r("file.read", { index: e, as: i }).then((w) => w[b]);
|
|
188
188
|
return {
|
|
189
189
|
...t,
|
|
190
|
-
text: () =>
|
|
191
|
-
bytes: () =>
|
|
192
|
-
stream: () =>
|
|
190
|
+
text: () => s("text", "text"),
|
|
191
|
+
bytes: () => s("bytes", "bytes"),
|
|
192
|
+
stream: () => s("stream", "stream"),
|
|
193
|
+
read: (i = "bytes") => r("file.read", { index: e, as: i }),
|
|
193
194
|
save: (i, b) => d.save(i, { index: e, mime: b })
|
|
194
195
|
};
|
|
195
196
|
},
|
|
196
197
|
async save(e, t = {}) {
|
|
197
|
-
const
|
|
198
|
-
return typeof t.through == "number" && (i.through = t.through),
|
|
198
|
+
const s = await f(e), i = { index: t.index ?? 0, data: s.data, mime: t.mime };
|
|
199
|
+
return typeof t.through == "number" && (i.through = t.through), t.csv && (i.csv = { ...t.csv }), t.beside && (i.beside = t.beside), t.conflict && (i.conflict = t.conflict), r("file.save", i, s.transfer);
|
|
199
200
|
},
|
|
200
|
-
async saveAs(e, t,
|
|
201
|
+
async saveAs(e, t, s) {
|
|
201
202
|
const i = await f(t);
|
|
202
|
-
return
|
|
203
|
+
return r("file.saveAs", { name: e, data: i.data, mime: s }, i.transfer);
|
|
203
204
|
},
|
|
204
|
-
async download(e, t,
|
|
205
|
+
async download(e, t, s) {
|
|
205
206
|
const i = await f(t);
|
|
206
|
-
return
|
|
207
|
+
return r("ui.download", { name: e, data: i.data, mime: s }, i.transfer);
|
|
207
208
|
},
|
|
208
209
|
async print(e, t) {
|
|
209
|
-
const
|
|
210
|
-
return
|
|
210
|
+
const s = await f(t);
|
|
211
|
+
return r("ui.print", { name: e, data: s.data, mime: "application/pdf" }, s.transfer);
|
|
211
212
|
},
|
|
212
213
|
coedit: {
|
|
213
214
|
join(e = 0) {
|
|
214
|
-
return
|
|
215
|
+
return r("coedit.join", { index: e });
|
|
215
216
|
},
|
|
216
217
|
subscribe(e) {
|
|
217
|
-
return
|
|
218
|
+
return r("coedit.subscribe", { from: e });
|
|
218
219
|
},
|
|
219
220
|
append(e, t) {
|
|
220
|
-
return
|
|
221
|
+
return r("coedit.append", { kind: e, body: t });
|
|
221
222
|
},
|
|
222
223
|
async lease(e, t) {
|
|
223
|
-
return !!(await
|
|
224
|
+
return !!(await r("coedit.lease", { op: e, changesSeen: t }))?.granted;
|
|
224
225
|
},
|
|
225
226
|
cursor(e) {
|
|
226
227
|
c("coedit.cursor", { cursor: e });
|
|
227
228
|
},
|
|
228
229
|
async putBlob(e, t) {
|
|
229
|
-
const
|
|
230
|
-
if (!(
|
|
231
|
-
await
|
|
230
|
+
const s = await f(t);
|
|
231
|
+
if (!(s.data instanceof ArrayBuffer)) throw new m({ code: "invalid", message: "a blob is bytes" });
|
|
232
|
+
await r("coedit.blob.put", { name: e, data: s.data }, s.transfer);
|
|
232
233
|
},
|
|
233
234
|
async getBlob(e) {
|
|
234
|
-
return (await
|
|
235
|
+
return (await r("coedit.blob.get", { name: e })).bytes;
|
|
235
236
|
},
|
|
236
237
|
async leave() {
|
|
237
|
-
await
|
|
238
|
+
await r("coedit.leave");
|
|
238
239
|
}
|
|
239
240
|
},
|
|
240
241
|
dirty(e) {
|
|
@@ -247,43 +248,43 @@ function T(n, u) {
|
|
|
247
248
|
c("ui.toast", { text: String(e).slice(0, x.maxLineChars), tone: t });
|
|
248
249
|
},
|
|
249
250
|
confirm(e) {
|
|
250
|
-
return
|
|
251
|
+
return r("ui.confirm", typeof e == "string" ? { text: e } : e);
|
|
251
252
|
},
|
|
252
253
|
close() {
|
|
253
254
|
c("ui.close");
|
|
254
255
|
},
|
|
255
256
|
copy(e) {
|
|
256
|
-
return
|
|
257
|
+
return r("clipboard.write", { text: String(e) });
|
|
257
258
|
},
|
|
258
259
|
call(e, t) {
|
|
259
|
-
return
|
|
260
|
+
return r("engine.call", { method: e, params: t });
|
|
260
261
|
},
|
|
261
262
|
submit(e, t) {
|
|
262
|
-
return
|
|
263
|
+
return r("job.submit", { action: e, params: t });
|
|
263
264
|
},
|
|
264
265
|
state: {
|
|
265
266
|
get(e) {
|
|
266
|
-
return
|
|
267
|
+
return r("state.get", { key: e });
|
|
267
268
|
},
|
|
268
269
|
set(e, t) {
|
|
269
|
-
return
|
|
270
|
+
return r("state.set", { key: e, value: t });
|
|
270
271
|
}
|
|
271
272
|
},
|
|
272
273
|
license: {
|
|
273
274
|
get() {
|
|
274
|
-
return
|
|
275
|
+
return r("license.get");
|
|
275
276
|
}
|
|
276
277
|
},
|
|
277
278
|
on(e, t) {
|
|
278
|
-
let
|
|
279
|
-
return
|
|
279
|
+
let s = l.get(e);
|
|
280
|
+
return s || l.set(e, s = /* @__PURE__ */ new Set()), s.add(t), () => s.delete(t);
|
|
280
281
|
},
|
|
281
282
|
onSave(e) {
|
|
282
283
|
return a = e, () => {
|
|
283
284
|
a === e && (a = null);
|
|
284
285
|
};
|
|
285
286
|
},
|
|
286
|
-
request:
|
|
287
|
+
request: r
|
|
287
288
|
};
|
|
288
289
|
return u.saveShortcut !== !1 && typeof document < "u" && document.addEventListener(
|
|
289
290
|
"keydown",
|
package/dist/filex-app-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get'\n /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */\n | 'ui.print'\n /**\n * Editing together (`coedit.*`, task #189): the session's log the host\n * seals, orders through filex's relay and opens again. Defined in filex\n * 0.55, which answers each `unavailable`; offered from 0.56 to an app\n * granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.\n */\n | 'coedit.join'\n | 'coedit.subscribe'\n | 'coedit.append'\n | 'coedit.lease'\n | 'coedit.cursor'\n | 'coedit.blob.put'\n | 'coedit.blob.get'\n | 'coedit.leave';\n\nexport const METHODS: readonly Method[] = [\n 'session.get',\n 'file.read',\n 'file.save',\n 'file.saveAs',\n 'ui.dirty',\n 'ui.title',\n 'ui.toast',\n 'ui.confirm',\n 'ui.close',\n 'clipboard.write',\n 'ui.download',\n 'engine.call',\n 'job.submit',\n 'state.get',\n 'state.set',\n 'license.get',\n 'ui.print',\n 'coedit.join',\n 'coedit.subscribe',\n 'coedit.append',\n 'coedit.lease',\n 'coedit.cursor',\n 'coedit.blob.put',\n 'coedit.blob.get',\n 'coedit.leave',\n];\n\n/** What the host tells the app, unasked. */\nexport type HostEvent =\n /** The interface's colours changed (light/dark, palette). `data: Theme`. */\n | 'theme'\n /** The reader's language changed. `data: {locale, dir}`. */\n | 'locale'\n /** The open file changed underneath (another tab, another person). */\n | 'file.changed'\n /** The person closed the frame's surroundings; last chance to say so. */\n | 'close.request'\n /** An administrator installed a new version of this app: reload to use it. */\n | 'app.updated'\n /** Editing together: the next entry of the session's log, opened. `data: CoEditEntry`. */\n | 'coedit.entry'\n /** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */\n | 'coedit.cursor'\n /**\n * Editing together: the host stopped handing entries over. `data:\n * CoEditDropped` - `gone`: the membership ended (join again); `broken`: an\n * entry did not check out (stop editing together). A connection that drops\n * for a moment is the host's own: it reconnects and says nothing.\n */\n | 'coedit.dropped';\n\n/** What the host may ask the app (and wait for). */\nexport type HostRequest =\n /** Save now (the host's Save button, a draft's \"Save to disk\"): the app\n * hands its document to `file.save` and answers when it is written. */\n 'save';\n\n/** Why a call was refused. */\nexport type ErrorCode =\n /** The app was not granted what the call needs (files:write for a save…). */\n | 'not_granted'\n /** No such file (an index past the opened files), no such key. */\n | 'not_found'\n /** The file cannot be written: a read-only storage, a view-only opening. */\n | 'read_only'\n /** The call's parameters are wrong. */\n | 'invalid'\n /** Over a limit (LIMITS). */\n | 'too_large'\n /** The server could not be reached or refused; `message` says what. */\n | 'failed'\n /** The person said no (a confirm, a save-as picker closed). */\n | 'cancelled'\n /** The host does not offer this here (no engine, no save handler). */\n | 'unavailable'\n | 'unknown_method';\n\nexport interface BridgeError {\n code: ErrorCode;\n message?: string;\n}\n\n/* ── messages ──────────────────────────────────────────────────────────── */\n\nexport interface HelloMessage {\n type: typeof HELLO;\n v: number;\n}\n\nexport interface PortMessage {\n type: typeof PORT;\n v: number;\n}\n\nexport interface RequestMessage {\n id: number;\n method: Method;\n params?: unknown;\n}\n\nexport interface ResponseMessage {\n id: number;\n result?: unknown;\n error?: BridgeError;\n}\n\nexport interface EventMessage {\n event: HostEvent;\n data?: unknown;\n}\n\nexport interface HostRequestMessage {\n hid: number;\n request: HostRequest;\n params?: unknown;\n}\n\nexport interface HostResponseMessage {\n hid: number;\n result?: unknown;\n error?: BridgeError;\n}\n\n/* ── shapes ────────────────────────────────────────────────────────────── */\n\n/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */\nexport interface Theme {\n mode: 'light' | 'dark';\n /** `--fe-bg`, `--fe-text`, `--fe-accent`… — the SDK sets them on `<html>`. */\n tokens: Record<string, string>;\n}\n\n/** One file the interface was opened with. Never a storage path. */\nexport interface FileInfo {\n index: number;\n name: string;\n /** Lower-case, no dot. */\n ext: string;\n size: number;\n mime: string;\n /** It cannot be saved over: read-only storage, view-only opening, or the\n * app holds no `files:write`. */\n readOnly: boolean;\n /**\n * 0.55.0: the file is end-to-end encrypted - `folder` in an encrypted\n * folder, `vault` in a vault (where only one person edits at a time),\n * `file` for a single encrypted file (`.fxe`). As the server says it;\n * ABSENT for a file that is not encrypted.\n *\n * ⚠ The server holds such a file only as ciphertext and has no key. Unless\n * `plaintext` is true, filex hands the app nothing of it: `file.read` is\n * refused (`failed` with `encrypted`), a save is refused, and `readOnly`\n * is true.\n */\n encrypted?: 'folder' | 'vault' | 'file';\n /**\n * 0.56.0: filex hands this encrypted file to the app in the clear. The\n * person's browser, which holds the folder key while the folder is\n * unlocked, decrypts it for `file.read` and encrypts what `file.save`\n * hands over before it leaves the browser; the server sees only\n * ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app\n * whose manifest says `encrypted_folders: {\"open\": true}` (the grant\n * `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.\n *\n * In a vault (`encrypted: 'vault'`) the browser reads the file from the\n * vault's packs and writes a save as the vault's next generation, under\n * the vault's write lock (one writer at a time). A save is refused -\n * `failed` with `vault_locked` - while somebody else writes the vault, and\n * with `vault_lock_lost` when this session's write lock ended before the\n * save was committed: nothing was written, save again. The vault locked\n * meanwhile (its keys left the browser): `unavailable`, and filex closes\n * the app.\n *\n * What changes for the app: a read and a save are held whole in the page\n * (at most LIMITS.maxEncryptedBytes); a save is written only over the file\n * as it was opened (`failed` with `changed` when it changed meanwhile);\n * `file.saveAs` answers `unavailable` (a copy elsewhere would leave the\n * folder decrypted); the app's module (`engine.call`, `job.submit`) is\n * never handed the file; `state.get` / `state.set` use a store of their\n * own for this folder, sealed in the person's browser (the server keeps it\n * and cannot read it; apart from the app's ordinary store, a little smaller\n * as it is measured sealed). Everything the app holds is plaintext: keep it\n * in the frame. In a folder whose names are encrypted too, `name` and\n * `ext` are the decrypted name's - the server never sees them.\n */\n plaintext?: true;\n}\n\n/** What `session.get` answers — what the app knows about where it runs. */\nexport interface Session {\n v: number;\n app: { name: string; version: string };\n view: { id: string; placement: 'modal' | 'page' | 'inspector' | 'home' | 'viewer' };\n /** The reader's language tag (`tr`, `pt-br`) and its direction. */\n locale: string;\n dir: 'ltr' | 'rtl';\n theme: Theme;\n /** The person's display name; never their e-mail, never a token. */\n user: { name: string };\n files: FileInfo[];\n /** The permissions this app was granted — so it can hide what it may not do. */\n grants: string[];\n /** The administrator's non-secret settings for this app (grant `settings`). */\n settings?: Record<string, string>;\n}\n\n/** `file.read` params. */\nexport interface ReadParams {\n index?: number;\n /** `stream` (a transferred ReadableStream<Uint8Array>), `bytes` (an\n * ArrayBuffer), or `text` (UTF-8, at most LIMITS.maxTextBytes). */\n as?: 'stream' | 'bytes' | 'text';\n}\n\nexport interface ReadResult {\n name: string;\n size: number;\n mime: string;\n stream?: ReadableStream<Uint8Array>;\n bytes?: ArrayBuffer;\n text?: string;\n}\n\n/** `file.save` params: the new content of the opened file. */\nexport interface SaveParams {\n index?: number;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n /**\n * 0.55.0, editing together: the last log entry (`CoEditEntry.seq`) the\n * saved document holds, so the session knows what is saved. Ignored\n * outside a session.\n */\n through?: number;\n}\n\nexport interface SaveResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.download`: a file for the person's own disk. filex does it (a sandboxed\n * frame cannot download), with the app's `ui:download` grant, on a gesture in\n * the frame or the person's yes, never over LIMITS.maxDownloadBytes.\n */\nexport interface DownloadParams {\n /** The file name offered — a name, not a path. */\n name: string;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface DownloadResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.print` (filex 0.55.0): a PDF for the browser's print dialog. A\n * sandboxed frame may not open that dialog (measured: Chromium ignores\n * `print()` there and says so, Firefox ignores it), so filex prints the PDF\n * from a page of its own, with the app's `ui:print` grant, never over\n * LIMITS.maxPrintBytes. filex asks every time, above the frame, and the\n * dialog opens only on the person's click on filex's Allow. The print dialog\n * can save the PDF too, so `ui:print` is the same kind of grant as\n * `ui:download`. filex 0.54 and older answer `unknown_method`.\n */\nexport interface PrintParams {\n /** The document's name, for the question filex may ask - a name, not a path. */\n name: string;\n /** The PDF: a transferred ArrayBuffer or ReadableStream<Uint8Array>. */\n data: ReadableStream<Uint8Array> | ArrayBuffer;\n /** `application/pdf` when given; nothing else is printed. */\n mime?: string;\n}\n\nexport interface PrintResult {\n /** The browser's print dialog was opened with the PDF. */\n printed: true;\n size: number;\n}\n\n/* ── editing together (coedit.*: filex 0.55 defined, 0.56 offers) ──────── */\n\n/** What a member appends: the editor's changes, a lock request, a release. */\nexport type CoEditKind = 'changes' | 'lock' | 'release';\n\n/** A member of the session. */\nexport interface CoEditMember {\n /** The relay's id for the connection. */\n client: string;\n /** An opaque id of the person, the same for each of their connections in\n * this session; never an account id. */\n user: string;\n /** The person's display name. */\n name: string;\n /** The editor's per-session user index: given once, never twice. */\n indexUser: number;\n /** May append changes (an editor role and the right to change the file). */\n canEdit: boolean;\n}\n\n/** `coedit.join` params: join (or start) the session of an opened file. */\nexport interface CoEditJoinParams {\n index?: number;\n}\n\n/**\n * What `coedit.join` answers: who the app is in the session and how far\n * the log has gone. The entries come as `coedit.entry` events once the app\n * subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`\n * when filex offers no editing together here (filex 0.55, editing together\n * turned off, an encrypted file, a vault): the app edits alone - the message\n * says why (`not_ready` after waiting for a session's creator, `encrypted`,\n * `nothing_to_watch` for a person who may only read the file while nobody\n * edits it, or the server's code: `coedit_off`, `coedit_build`,\n * `coedit_file_changed`).\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /**\n * The last entry's seq when filex handed the session over (0: an empty\n * log). filex 0.56: the changes this browser kept from an earlier opening\n * of the file (a lost connection, a closed tab), which filex sends before\n * it answers, are in the log up to here - an editor that starts once it\n * has read this far opens with them.\n */\n head: number;\n /** The last `changes` entry's seq, as far as `head`. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\n /**\n * filex 0.56: this member started the session. Before anybody else can\n * join it puts the base document - EXACTLY the bytes it opened - with\n * `coedit.blob.put('base', bytes)`. A member that did not start it opens\n * `coedit.blob.get('base')`, never the file: the file may hold a save of\n * the session already, and the log applies to the base.\n */\n created: boolean;\n}\n\n/** `coedit.dropped` event data (filex 0.56). */\nexport interface CoEditDropped {\n /** The last entry the host handed over. */\n from: number;\n /** `gone`: the membership ended (the session closed, or the page was away\n * too long) - join again. `broken`: an entry did not check out (missing,\n * moved, altered, replayed) - stop editing together. */\n reason: 'gone' | 'broken';\n}\n\n/** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */\nexport interface CoEditSubscribeParams {\n from: number;\n}\n\n/** `coedit.append` params: the host seals it and the relay places it. */\nexport interface CoEditAppendParams {\n kind: CoEditKind;\n body: unknown;\n}\n\nexport interface CoEditAppendResult {\n /** Where it landed; it comes back as a `coedit.entry` too. */\n seq: number;\n}\n\n/** `coedit.lease`: the changes lease (one writer at a time, one that has\n * seen every change). */\nexport interface CoEditLeaseParams {\n op: 'acquire' | 'release';\n /** The seq of the last `changes` entry the app applied. */\n changesSeen: number;\n}\n\nexport interface CoEditLeaseResult {\n granted: boolean;\n}\n\n/** `coedit.cursor` params: passed to the other members, not kept. The host\n * sends a few a second at most (the last one wins). */\nexport interface CoEditCursorParams {\n cursor: unknown;\n}\n\n/** `coedit.blob.put` / `coedit.blob.get`: the session's sealed blobs (the\n * base document, images added while editing). A name, not a path. */\nexport interface CoEditBlobPutParams {\n name: string;\n data: ArrayBuffer;\n}\n\nexport interface CoEditBlobGetParams {\n name: string;\n}\n\nexport interface CoEditBlobGetResult {\n name: string;\n bytes: ArrayBuffer;\n}\n\ninterface CoEditEntryBase {\n seq: number;\n /** The relay's time, Unix ms - the same for every member. */\n at: number;\n client: string;\n}\n\n/** One entry of the session's log, opened and checked by filex. */\nexport type CoEditEntry =\n | (CoEditEntryBase & { kind: CoEditKind; body: unknown })\n | (CoEditEntryBase & { kind: 'join'; member: CoEditMember })\n | (CoEditEntryBase & { kind: 'leave' })\n | (CoEditEntryBase & { kind: 'saved'; through: number });\n\n/** `coedit.cursor` event data. */\nexport interface CoEditCursor {\n client: string;\n cursor: unknown;\n}\n\n/**\n * `file.saveAs`: a NEW file, in a folder the person picks in filex's own\n * folder dialog (the one its Move to… uses), which opens in the opened\n * file's folder. Closing the dialog answers `cancelled`.\n */\nexport interface SaveAsParams {\n /** The file name — a name, not a path. */\n name: string;\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveAsResult {\n saved: true;\n name: string;\n size: number;\n}\n\nexport interface ToastParams {\n text: string;\n tone?: 'info' | 'success' | 'warning' | 'error';\n}\n\nexport interface ConfirmParams {\n title?: string;\n text: string;\n confirm?: string;\n cancel?: string;\n danger?: boolean;\n}\n\n/**\n * `license.get` (filex 0.52.0): what the app's license says. A paid app is\n * installed from a store, which issues its license and is asked about it every\n * day; filex keeps the key and never hands it to the app. `status`:\n *\n * - `free`: not a paid app;\n * - `valid`: the store said valid at its last check;\n * - `grace`: valid, but the store could not be asked since - it holds until\n * the grace the store signed ends;\n * - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,\n * `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held\n * and its interface is not served, so an app rarely reads one of these.\n *\n * `valid_until` / `updates_until` are there for a valid license (dates as\n * RFC 3339). Who holds the license is not the app's to read. filex 0.51.0\n * and older answer `unknown_method`.\n */\nexport interface LicenseInfo {\n status: string;\n valid_until?: string;\n updates_until?: string;\n}\n\n/** `engine.call`: the app's own module, `ui_call` export. */\nexport interface EngineCallParams {\n method: string;\n params?: unknown;\n}\n\n/** `job.submit`: queue one of the app's actions on the opened files. */\nexport interface JobSubmitParams {\n action: string;\n params?: Record<string, unknown>;\n}\n\n/* ── limits ────────────────────────────────────────────────────────────── */\n\nexport const LIMITS = {\n /** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */\n maxTextBytes: 32 << 20,\n /** One `state.set` value, as JSON. */\n maxStateBytes: 8 << 10,\n /** A `ui.toast` / `ui.title` text, in characters. */\n maxLineChars: 300,\n /** How long the SDK waits for the host's port. */\n connectTimeoutMs: 10_000,\n /** The largest file `ui.download` hands the person (streamed or not). */\n maxDownloadBytes: 256 << 20,\n /**\n * The largest PDF `ui.print` prints (filex 0.55). Lower than a download:\n * a download can stream to disk, a print is held whole in the page (one\n * Blob) until the print dialog has read it, and a document worth printing\n * is far smaller.\n */\n maxPrintBytes: 64 << 20,\n /**\n * The largest file of an encrypted folder filex hands an app in the clear\n * or takes back from it (filex 0.56, `FileInfo.plaintext`): it is\n * decrypted and encrypted whole in the page, as filex's own explorer does\n * for a file of this size.\n */\n maxEncryptedBytes: 200 << 20,\n} as const;\n\n/* ── guards ────────────────────────────────────────────────────────────── */\n\nexport function isHello(d: unknown): d is HelloMessage {\n return !!d && typeof d === 'object' && (d as HelloMessage).type === HELLO && typeof (d as HelloMessage).v === 'number';\n}\n\nexport function isPortMessage(d: unknown): d is PortMessage {\n return !!d && typeof d === 'object' && (d as PortMessage).type === PORT;\n}\n\nexport function isRequest(d: unknown): d is RequestMessage {\n return (\n !!d &&\n typeof d === 'object' &&\n typeof (d as RequestMessage).id === 'number' &&\n typeof (d as RequestMessage).method === 'string'\n );\n}\n\nexport function isKnownMethod(m: string): m is Method {\n return (METHODS as readonly string[]).includes(m);\n}\n","/**\n * @brftech/filex-app-ui — the SDK an app's own interface bundles.\n *\n * ```js\n * import { connect } from '@brftech/filex-app-ui';\n *\n * const fx = await connect(); // the handshake with filex\n * const file = await fx.open(); // the file the app was opened with\n * editor.setValue(await file.text());\n * editor.onChange(() => fx.dirty(true));\n * fx.onSave(async () => editor.getValue()); // filex's Save, a draft's \"Save to disk\", Ctrl+S\n * ```\n *\n * No dependency, a few KB. The interface runs in a sandboxed frame with no\n * network and no storage of its own: every call below goes to filex over the\n * MessagePort the handshake set up, and filex decides it (docs/APP-PLUGINS.md\n * → \"An app's own interface\"). The protocol itself is `./protocol`.\n */\nimport {\n BRIDGE_VERSION,\n HELLO,\n LIMITS,\n isPortMessage,\n type BridgeError,\n type ConfirmParams,\n type ErrorCode,\n type FileInfo,\n type HostEvent,\n type HostRequestMessage,\n type Method,\n type ReadResult,\n type SaveAsResult,\n type SaveResult,\n type Session,\n type Theme,\n type ToastParams,\n type DownloadResult,\n type LicenseInfo,\n type PrintResult,\n type CoEditHello,\n type CoEditKind,\n type CoEditAppendResult,\n type CoEditLeaseResult,\n type CoEditBlobGetResult,\n} from './protocol';\n\nexport * from './protocol';\n\n/** A call filex refused, with the reason as a code. */\nexport class FilexError extends Error {\n readonly code: ErrorCode;\n constructor(err: BridgeError) {\n super(err.message || err.code);\n this.name = 'FilexError';\n this.code = err.code;\n }\n}\n\n/** Content the app hands to a save. */\nexport type SaveData = string | Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>;\n\n/** One opened file, read the way the app needs it. */\nexport interface OpenedFile extends FileInfo {\n text(): Promise<string>;\n bytes(): Promise<ArrayBuffer>;\n stream(): Promise<ReadableStream<Uint8Array>>;\n /** Save new content over this file (a new version, or the draft it is). */\n save(data: SaveData, mime?: string): Promise<SaveResult>;\n}\n\nexport interface ConnectOptions {\n /** How long to wait for filex (default 10 s). */\n timeoutMs?: number;\n /**\n * Put filex's colours (`--fe-*`), `lang`, `dir` and `data-theme` on\n * `<html>`, and keep them current (default true).\n */\n applyTheme?: boolean;\n /** Ctrl/Cmd+S inside the interface runs the save handler (default true). */\n saveShortcut?: boolean;\n}\n\nexport interface FilexApp {\n readonly session: Session;\n /** The file at `index` (default the first) the interface was opened with. */\n open(index?: number): Promise<OpenedFile>;\n /**\n * Save new content over an opened file. `through` (filex 0.56, editing\n * together): the last log entry the saved document holds, so the session\n * knows what is saved.\n */\n save(data: SaveData, opts?: { index?: number; mime?: string; through?: number }): Promise<SaveResult>;\n /** Save a NEW file; filex asks the person where (its own folder picker). */\n saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;\n /** Unsaved changes, or not: filex asks before the person leaves them. */\n dirty(on: boolean): void;\n title(text: string): void;\n toast(text: string, tone?: ToastParams['tone']): void;\n confirm(opts: ConfirmParams | string): Promise<boolean>;\n /** Ask filex to close the interface (it asks first when there are changes). */\n close(): void;\n /** Put text on the clipboard (filex does it, the same way in every browser). */\n copy(text: string): Promise<void>;\n /**\n * Hand the person a file for their own disk (the app needs `ui.download`\n * in its manifest). Call it from a click or a key press: without one,\n * filex asks the person first. `cancelled` when they say no.\n */\n download(name: string, data: SaveData, mime?: string): Promise<DownloadResult>;\n /**\n * Print a PDF (filex 0.55; the app needs `ui.print` in its manifest): a\n * sandboxed frame may not open the print dialog, so filex prints it from\n * its own page. filex asks the person every time, and the dialog opens on\n * their click on filex's Allow; at most LIMITS.maxPrintBytes. filex 0.54\n * and older answer `unknown_method`, and a host whose print page may not\n * be shown `unavailable`: hand the PDF over with `download` then.\n */\n print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;\n /**\n * Editing together (filex 0.56; the app's manifest says `\"co_edit\":\n * {\"open\": true}`, granted as `files:co-edit`). filex seals what the app appends, orders it\n * through its relay and hands every member the same entries in the same\n * order (`coedit.entry` events after `subscribe`); the app never holds a\n * key. `join` answering `unavailable` (filex 0.55, editing together off\n * here, an encrypted file) means: edit alone. The member that started the\n * session (`created`) puts the base document before anybody else may join.\n */\n coedit: {\n join(index?: number): Promise<CoEditHello>;\n subscribe(from: number): Promise<void>;\n append(kind: CoEditKind, body: unknown): Promise<CoEditAppendResult>;\n lease(op: 'acquire' | 'release', changesSeen: number): Promise<boolean>;\n cursor(cursor: unknown): void;\n putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView): Promise<void>;\n getBlob(name: string): Promise<ArrayBuffer>;\n leave(): Promise<void>;\n };\n /** Call the app's own module (`ui_call` export) with the opened files. */\n call<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Queue one of the app's actions on the opened files; answers the op. */\n submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;\n /**\n * This person's small store for this app (JSON values, 8 KiB each). While\n * the app holds a file of an encrypted folder in the clear\n * (`FileInfo.plaintext`, filex 0.56) it is another store, sealed in the\n * browser - apart from this one, and empty while the folder is locked.\n */\n state: {\n get<T = unknown>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n };\n /**\n * The app's license (filex 0.52.0; a paid app installed from a store):\n * `{status: \"free\"}` for a free app, `{status: \"valid\", valid_until,\n * updates_until}` for a licensed one. Never the key, never the licensee.\n */\n license: {\n get(): Promise<LicenseInfo>;\n };\n /** Listen to filex. Returns the unsubscribe. */\n on(event: HostEvent, handler: (data: unknown) => void): () => void;\n /**\n * What to save when filex asks (its Save button, a draft's \"Save to disk\",\n * Ctrl+S): return the document and the SDK saves it over the opened file;\n * return nothing if the handler saved by itself.\n */\n onSave(handler: () => SaveData | void | Promise<SaveData | void>): () => void;\n /** Raw call, for a method this SDK has no helper for. */\n request<T = unknown>(method: Method, params?: unknown, transfer?: Transferable[]): Promise<T>;\n}\n\nlet connecting: Promise<FilexApp> | null = null;\n\n/**\n * The handshake. Resolves once filex answered with its port and the session;\n * rejects outside filex or when filex did not answer in time. Calling it again\n * answers the same connection.\n */\nexport function connect(opts: ConnectOptions = {}): Promise<FilexApp> {\n if (!connecting) connecting = open(opts);\n return connecting;\n}\n\nfunction open(opts: ConnectOptions): Promise<FilexApp> {\n return new Promise<FilexApp>((resolve, reject) => {\n if (typeof window === 'undefined' || window.parent === window) {\n reject(new FilexError({ code: 'unavailable', message: 'not running inside filex' }));\n return;\n }\n const parent = window.parent;\n // Where filex runs, when the browser says so (Chrome, Safari). A frame\n // drawn with referrerpolicy=\"no-referrer\" has no document.referrer, and\n // Firefox has no ancestorOrigins: then `source === parent` is the check,\n // which is the one that matters (only the parent window IS the parent).\n const expected = ancestorOrigin();\n const timer = setTimeout(() => {\n window.removeEventListener('message', onMessage);\n reject(new FilexError({ code: 'unavailable', message: 'filex did not answer' }));\n }, opts.timeoutMs ?? LIMITS.connectTimeoutMs);\n\n function onMessage(ev: MessageEvent) {\n if (ev.source !== parent || !isPortMessage(ev.data) || !ev.ports[0]) return;\n if (expected && ev.origin !== expected) return;\n window.removeEventListener('message', onMessage);\n clearTimeout(timer);\n const port = ev.ports[0];\n const app = bridge(port, opts);\n app\n .request<Session>('session.get')\n .then((session) => {\n (app as { session: Session }).session = session;\n if (opts.applyTheme !== false) {\n applyLook(session.theme, session.locale, session.dir);\n app.on('theme', (t) => applyLook(t as Theme));\n app.on('locale', (l) => {\n const v = l as { locale?: string; dir?: 'ltr' | 'rtl' };\n applyLook(undefined, v.locale, v.dir);\n });\n }\n resolve(app);\n })\n .catch(reject);\n }\n window.addEventListener('message', onMessage);\n parent.postMessage({ type: HELLO, v: BRIDGE_VERSION }, '*');\n });\n}\n\nfunction ancestorOrigin(): string {\n try {\n const list = (window.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins;\n if (list && list.length > 0 && list[0] !== 'null') return list[0];\n } catch {\n /* no such API */\n }\n return '';\n}\n\nfunction bridge(port: MessagePort, opts: ConnectOptions): FilexApp {\n let seq = 0;\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();\n const listeners = new Map<HostEvent, Set<(data: unknown) => void>>();\n let saveHandler: (() => SaveData | void | Promise<SaveData | void>) | null = null;\n\n port.onmessage = (ev: MessageEvent) => {\n const m = ev.data as Record<string, unknown> | null;\n if (!m || typeof m !== 'object') return;\n if (typeof m.id === 'number') {\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n if (m.error) p.reject(new FilexError(m.error as BridgeError));\n else p.resolve(m.result);\n return;\n }\n if (typeof m.hid === 'number') {\n void answerHost(m as unknown as HostRequestMessage);\n return;\n }\n if (typeof m.event === 'string') {\n for (const h of listeners.get(m.event as HostEvent) ?? []) {\n try {\n h(m.data);\n } catch (e) {\n console.error('[filex-app-ui] listener failed', e);\n }\n }\n }\n };\n\n async function answerHost(m: HostRequestMessage) {\n if (m.request === 'save') {\n if (!saveHandler) {\n port.postMessage({ hid: m.hid, error: { code: 'unavailable', message: 'this app has no save handler' } });\n return;\n }\n try {\n const res = await runSave();\n port.postMessage({ hid: m.hid, result: res ?? { saved: true } });\n } catch (e) {\n const err = e instanceof FilexError ? { code: e.code, message: e.message } : { code: 'failed', message: String((e as Error)?.message ?? e) };\n port.postMessage({ hid: m.hid, error: err });\n }\n return;\n }\n port.postMessage({ hid: m.hid, error: { code: 'unknown_method' } });\n }\n\n async function runSave(): Promise<SaveResult | null> {\n if (!saveHandler) return null;\n const data = await saveHandler();\n if (data === undefined || data === null) return null;\n return app.save(data as SaveData);\n }\n\n function request<T>(method: Method, params?: unknown, transfer: Transferable[] = []): Promise<T> {\n const id = ++seq;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject });\n try {\n port.postMessage({ id, method, params }, transfer);\n } catch (e) {\n pending.delete(id);\n reject(e);\n }\n });\n }\n\n function notify(method: Method, params?: unknown) {\n request(method, params).catch((e) => console.warn('[filex-app-ui]', method, e));\n }\n\n async function payload(data: SaveData): Promise<{ data: ReadableStream<Uint8Array> | ArrayBuffer | string; transfer: Transferable[] }> {\n if (typeof data === 'string') return { data, transfer: [] };\n if (data instanceof ArrayBuffer) return { data, transfer: [data] };\n if (ArrayBuffer.isView(data)) {\n const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n return { data: copy, transfer: [copy] };\n }\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n const buf = await data.arrayBuffer();\n return { data: buf, transfer: [buf] };\n }\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {\n return { data, transfer: [data as unknown as Transferable] };\n }\n throw new FilexError({ code: 'invalid', message: 'save takes a string, a Blob, bytes or a ReadableStream' });\n }\n\n const app: FilexApp = {\n session: undefined as unknown as Session,\n async open(index = 0) {\n const info = app.session?.files?.[index];\n if (!info) throw new FilexError({ code: 'not_found', message: `no file at ${index}` });\n const read = <K extends keyof ReadResult>(as: 'stream' | 'bytes' | 'text', key: K) =>\n request<ReadResult>('file.read', { index, as }).then((r) => r[key] as NonNullable<ReadResult[K]>);\n return {\n ...info,\n text: () => read('text', 'text'),\n bytes: () => read('bytes', 'bytes'),\n stream: () => read('stream', 'stream'),\n save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),\n };\n },\n async save(data, o = {}) {\n const p = await payload(data);\n const params: Record<string, unknown> = { index: o.index ?? 0, data: p.data, mime: o.mime };\n if (typeof o.through === 'number') params.through = o.through;\n return request<SaveResult>('file.save', params, p.transfer);\n },\n async saveAs(name, data, mime) {\n const p = await payload(data);\n return request<SaveAsResult>('file.saveAs', { name, data: p.data, mime }, p.transfer);\n },\n async download(name, data, mime) {\n const p = await payload(data);\n return request<DownloadResult>('ui.download', { name, data: p.data, mime }, p.transfer);\n },\n async print(name, pdf) {\n const p = await payload(pdf);\n return request<PrintResult>('ui.print', { name, data: p.data, mime: 'application/pdf' }, p.transfer);\n },\n coedit: {\n join(index = 0) {\n return request<CoEditHello>('coedit.join', { index });\n },\n subscribe(from: number) {\n return request<void>('coedit.subscribe', { from });\n },\n append(kind: CoEditKind, body: unknown) {\n return request<CoEditAppendResult>('coedit.append', { kind, body });\n },\n async lease(op: 'acquire' | 'release', changesSeen: number) {\n const r = await request<CoEditLeaseResult>('coedit.lease', { op, changesSeen });\n return !!r?.granted;\n },\n cursor(cursor: unknown) {\n notify('coedit.cursor', { cursor });\n },\n async putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView) {\n const p = await payload(data);\n if (!(p.data instanceof ArrayBuffer)) throw new FilexError({ code: 'invalid', message: 'a blob is bytes' });\n await request<void>('coedit.blob.put', { name, data: p.data }, p.transfer);\n },\n async getBlob(name: string) {\n const r = await request<CoEditBlobGetResult>('coedit.blob.get', { name });\n return r.bytes;\n },\n async leave() {\n await request<void>('coedit.leave');\n },\n },\n dirty(on) {\n notify('ui.dirty', { dirty: !!on });\n },\n title(text) {\n notify('ui.title', { text: String(text).slice(0, LIMITS.maxLineChars) });\n },\n toast(text, tone) {\n notify('ui.toast', { text: String(text).slice(0, LIMITS.maxLineChars), tone });\n },\n confirm(o) {\n const params = typeof o === 'string' ? { text: o } : o;\n return request<boolean>('ui.confirm', params);\n },\n close() {\n notify('ui.close');\n },\n copy(text) {\n return request<void>('clipboard.write', { text: String(text) });\n },\n call<T>(method: string, params?: unknown) {\n return request<T>('engine.call', { method, params });\n },\n submit(action, params) {\n return request<{ op: unknown }>('job.submit', { action, params });\n },\n state: {\n get<T>(key: string) {\n return request<T | undefined>('state.get', { key });\n },\n set(key: string, value: unknown) {\n return request<void>('state.set', { key, value });\n },\n },\n license: {\n get() {\n return request<LicenseInfo>('license.get');\n },\n },\n on(event, handler) {\n let set = listeners.get(event);\n if (!set) listeners.set(event, (set = new Set()));\n set.add(handler);\n return () => set!.delete(handler);\n },\n onSave(handler) {\n saveHandler = handler;\n return () => {\n if (saveHandler === handler) saveHandler = null;\n };\n },\n request,\n };\n\n if (opts.saveShortcut !== false && typeof document !== 'undefined') {\n // ⚠ The keystroke is handled HERE and never forwarded: a frame that could\n // send key events to filex could press filex's own shortcuts. Ctrl+S runs\n // the app's own save handler, which saves through the bridge like any call.\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 's' || e.key === 'S') && saveHandler) {\n e.preventDefault();\n runSave().catch((err) => app.toast(String((err as Error)?.message ?? err), 'error'));\n }\n },\n true,\n );\n }\n return app;\n}\n\n/** Paint filex's look onto the interface's own document. */\nfunction applyLook(theme?: Theme, locale?: string, dir?: 'ltr' | 'rtl') {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (theme) {\n root.dataset.theme = theme.mode;\n root.style.colorScheme = theme.mode;\n for (const [k, v] of Object.entries(theme.tokens ?? {})) {\n if (/^--fe-[a-z0-9-]+$/.test(k) && typeof v === 'string') root.style.setProperty(k, v);\n }\n }\n if (locale) root.lang = locale;\n if (dir) root.dir = dir;\n}\n"],"names":["BRIDGE_VERSION","HELLO","PORT","METHODS","LIMITS","isHello","d","isPortMessage","isRequest","isKnownMethod","m","FilexError","err","connecting","connect","opts","open","resolve","reject","parent","expected","ancestorOrigin","timer","onMessage","ev","port","app","bridge","session","applyLook","t","l","v","list","seq","pending","listeners","saveHandler","p","answerHost","h","e","res","runSave","data","request","method","params","transfer","id","notify","payload","copy","buf","index","info","read","as","key","r","mime","o","name","pdf","from","kind","body","op","changesSeen","cursor","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get'\n /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */\n | 'ui.print'\n /**\n * Editing together (`coedit.*`, task #189): the session's log the host\n * seals, orders through filex's relay and opens again. Defined in filex\n * 0.55, which answers each `unavailable`; offered from 0.56 to an app\n * granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.\n */\n | 'coedit.join'\n | 'coedit.subscribe'\n | 'coedit.append'\n | 'coedit.lease'\n | 'coedit.cursor'\n | 'coedit.blob.put'\n | 'coedit.blob.get'\n | 'coedit.leave';\n\nexport const METHODS: readonly Method[] = [\n 'session.get',\n 'file.read',\n 'file.save',\n 'file.saveAs',\n 'ui.dirty',\n 'ui.title',\n 'ui.toast',\n 'ui.confirm',\n 'ui.close',\n 'clipboard.write',\n 'ui.download',\n 'engine.call',\n 'job.submit',\n 'state.get',\n 'state.set',\n 'license.get',\n 'ui.print',\n 'coedit.join',\n 'coedit.subscribe',\n 'coedit.append',\n 'coedit.lease',\n 'coedit.cursor',\n 'coedit.blob.put',\n 'coedit.blob.get',\n 'coedit.leave',\n];\n\n/** What the host tells the app, unasked. */\nexport type HostEvent =\n /** The interface's colours changed (light/dark, palette). `data: Theme`. */\n | 'theme'\n /** The reader's language changed. `data: {locale, dir}`. */\n | 'locale'\n /** The open file changed underneath (another tab, another person). */\n | 'file.changed'\n /** The person closed the frame's surroundings; last chance to say so. */\n | 'close.request'\n /** An administrator installed a new version of this app: reload to use it. */\n | 'app.updated'\n /** Editing together: the next entry of the session's log, opened. `data: CoEditEntry`. */\n | 'coedit.entry'\n /** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */\n | 'coedit.cursor'\n /**\n * Editing together: the host stopped handing entries over. `data:\n * CoEditDropped` - `gone`: the membership ended (join again); `broken`: an\n * entry did not check out (stop editing together). A connection that drops\n * for a moment is the host's own: it reconnects and says nothing.\n */\n | 'coedit.dropped';\n\n/** What the host may ask the app (and wait for). */\nexport type HostRequest =\n /** Save now (the host's Save button, a draft's \"Save to disk\"): the app\n * hands its document to `file.save` and answers when it is written. */\n 'save';\n\n/** Why a call was refused. */\nexport type ErrorCode =\n /** The app was not granted what the call needs (files:write for a save…). */\n | 'not_granted'\n /** No such file (an index past the opened files), no such key. */\n | 'not_found'\n /** The file cannot be written: a read-only storage, a view-only opening. */\n | 'read_only'\n /** The call's parameters are wrong. */\n | 'invalid'\n /** Over a limit (LIMITS). */\n | 'too_large'\n /** The server could not be reached or refused; `message` says what. */\n | 'failed'\n /** The person said no (a confirm, a save-as picker closed). */\n | 'cancelled'\n /** The host does not offer this here (no engine, no save handler). */\n | 'unavailable'\n | 'unknown_method';\n\nexport interface BridgeError {\n code: ErrorCode;\n message?: string;\n}\n\n/* ── messages ──────────────────────────────────────────────────────────── */\n\nexport interface HelloMessage {\n type: typeof HELLO;\n v: number;\n}\n\nexport interface PortMessage {\n type: typeof PORT;\n v: number;\n}\n\nexport interface RequestMessage {\n id: number;\n method: Method;\n params?: unknown;\n}\n\nexport interface ResponseMessage {\n id: number;\n result?: unknown;\n error?: BridgeError;\n}\n\nexport interface EventMessage {\n event: HostEvent;\n data?: unknown;\n}\n\nexport interface HostRequestMessage {\n hid: number;\n request: HostRequest;\n params?: unknown;\n}\n\nexport interface HostResponseMessage {\n hid: number;\n result?: unknown;\n error?: BridgeError;\n}\n\n/* ── shapes ────────────────────────────────────────────────────────────── */\n\n/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */\nexport interface Theme {\n mode: 'light' | 'dark';\n /** `--fe-bg`, `--fe-text`, `--fe-accent`… — the SDK sets them on `<html>`. */\n tokens: Record<string, string>;\n}\n\n/** One file the interface was opened with. Never a storage path. */\nexport interface FileInfo {\n index: number;\n name: string;\n /** Lower-case, no dot. */\n ext: string;\n size: number;\n mime: string;\n /** It cannot be saved over: read-only storage, view-only opening, or the\n * app holds no `files:write`. */\n readOnly: boolean;\n /**\n * 0.55.0: the file is end-to-end encrypted - `folder` in an encrypted\n * folder, `vault` in a vault (where only one person edits at a time),\n * `file` for a single encrypted file (`.fxe`). As the server says it;\n * ABSENT for a file that is not encrypted.\n *\n * ⚠ The server holds such a file only as ciphertext and has no key. Unless\n * `plaintext` is true, filex hands the app nothing of it: `file.read` is\n * refused (`failed` with `encrypted`), a save is refused, and `readOnly`\n * is true.\n */\n encrypted?: 'folder' | 'vault' | 'file';\n /**\n * 0.56.0: filex hands this encrypted file to the app in the clear. The\n * person's browser, which holds the folder key while the folder is\n * unlocked, decrypts it for `file.read` and encrypts what `file.save`\n * hands over before it leaves the browser; the server sees only\n * ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app\n * whose manifest says `encrypted_folders: {\"open\": true}` (the grant\n * `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.\n *\n * In a vault (`encrypted: 'vault'`) the browser reads the file from the\n * vault's packs and writes a save as the vault's next generation, under\n * the vault's write lock (one writer at a time). A save is refused -\n * `failed` with `vault_locked` - while somebody else writes the vault, and\n * with `vault_lock_lost` when this session's write lock ended before the\n * save was committed: nothing was written, save again. The vault locked\n * meanwhile (its keys left the browser): `unavailable`, and filex closes\n * the app.\n *\n * What changes for the app: a read and a save are held whole in the page\n * (at most LIMITS.maxEncryptedBytes); a save is written only over the file\n * as it was opened (`failed` with `changed` when it changed meanwhile);\n * `file.saveAs` answers `unavailable` (a copy elsewhere would leave the\n * folder decrypted); the app's module (`engine.call`, `job.submit`) is\n * never handed the file; `state.get` / `state.set` use a store of their\n * own for this folder, sealed in the person's browser (the server keeps it\n * and cannot read it; apart from the app's ordinary store, a little smaller\n * as it is measured sealed). Everything the app holds is plaintext: keep it\n * in the frame. In a folder whose names are encrypted too, `name` and\n * `ext` are the decrypted name's - the server never sees them.\n */\n plaintext?: true;\n}\n\n/** What `session.get` answers — what the app knows about where it runs. */\nexport interface Session {\n v: number;\n app: { name: string; version: string };\n view: { id: string; placement: 'modal' | 'page' | 'inspector' | 'home' | 'viewer' };\n /** The reader's language tag (`tr`, `pt-br`) and its direction. */\n locale: string;\n dir: 'ltr' | 'rtl';\n theme: Theme;\n /**\n * The person's display name (their profile's, else their username); never\n * their e-mail, never a token. '' where nobody is signed in (a public\n * page). An editor writes it as the author of comments and tracked\n * changes.\n */\n user: { name: string };\n /**\n * filex 0.57: the language the administrator fixed for document editors\n * (Admin → External services → ONLYOFFICE → Editor language,\n * `FILEX_ONLYOFFICE_LANG`; a code such as `de`, `pt-PT`, `zh-TW`). An app\n * that draws a document editor draws it in this language rather than the\n * reader's `locale`; its own words stay in `locale`. Absent: each person's\n * own language decides.\n */\n editor_lang?: string;\n files: FileInfo[];\n /** The permissions this app was granted — so it can hide what it may not do. */\n grants: string[];\n /** The administrator's non-secret settings for this app (grant `settings`). */\n settings?: Record<string, string>;\n}\n\n/** `file.read` params. */\nexport interface ReadParams {\n index?: number;\n /** `stream` (a transferred ReadableStream<Uint8Array>), `bytes` (an\n * ArrayBuffer), or `text` (UTF-8, at most LIMITS.maxTextBytes). */\n as?: 'stream' | 'bytes' | 'text';\n}\n\n/**\n * How a CSV file is written (filex 0.57): `ReadResult.text_dialect` of a\n * `.csv`, read by the server from the file's own bytes.\n */\nexport interface TextDialect {\n /** One character: `,`, `;`, `\\t` or `|`. */\n delimiter: string;\n /** The file starts with a byte order mark. */\n bom: boolean;\n /** Its lines end in CRLF (else LF). */\n crlf: boolean;\n /**\n * Its encoding as a browser's TextDecoder names it: `utf-8`, `utf-16le`,\n * `utf-16be`, or a code page (`windows-1250`, `windows-1251`,\n * `windows-1252`, `windows-1254`). `file.read` with `as: 'text'` decodes\n * with it; `bytes` and `stream` hand the bytes as they are.\n */\n encoding: string;\n}\n\nexport interface ReadResult {\n name: string;\n size: number;\n mime: string;\n stream?: ReadableStream<Uint8Array>;\n bytes?: ArrayBuffer;\n text?: string;\n /**\n * filex 0.57: how a `.csv` is written (its delimiter, byte order mark,\n * line ends, encoding). Absent for any other file, for a file of an\n * encrypted folder (the server holds its ciphertext only), and from filex\n * 0.56 and older.\n */\n text_dialect?: TextDialect;\n}\n\n/**\n * What an app says about the CSV it saves (filex 0.57; `SaveParams.csv`):\n * how ITS bytes are written. filex reads the same from the bytes and refuses\n * a save they contradict (`invalid`, message `csv_dialect`): nothing is\n * written. Whatever the app wrote, the file is written in its own way - its\n * delimiter, byte order mark, line ends and encoding - with every cell nobody\n * changed as the file has it. A field left out is not checked; no `csv` at\n * all is read from the bytes alone.\n */\nexport interface CsvDeclaration {\n /** One character: `,`, `;`, `\\t` or `|`. */\n delimiter?: string;\n /** As TextDialect.encoding names it (`utf-8` for a string the SDK sent). */\n encoding?: string;\n /** The bytes start with a byte order mark. */\n bom?: boolean;\n /** Their line ends. */\n eol?: 'crlf' | 'lf';\n}\n\n/** `file.save` params: the new content of the opened file. */\nexport interface SaveParams {\n index?: number;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n /**\n * 0.55.0, editing together: the last log entry (`CoEditEntry.seq`) the\n * saved document holds, so the session knows what is saved. Ignored\n * outside a session.\n */\n through?: number;\n /**\n * filex 0.57: what the app says about the CSV it saves over a `.csv`\n * (CsvDeclaration). Ignored for any other file and for a file of an\n * encrypted folder; filex 0.56 and older ignore it.\n */\n csv?: CsvDeclaration;\n /**\n * filex 0.57: the save is in this format (an extension: `docx`, `xlsx`,\n * `pptx`, `docm`, `xlsm`, `pptm`, `odt`, `ods`, `odp`), which the opened\n * file cannot hold (a `.doc`, `.xls`, `.ppt`...). It is written BESIDE the\n * file, which is left as it is, under a name filex chooses - the file's own\n * name with that extension, `rapor (2).docx` when that is taken - and only\n * for a person who may create files in that folder (`permission_denied`\n * otherwise). `SaveResult.name` says where; later saves of the same\n * opening go to that file. The same extension as the file's own: an\n * ordinary save over it. Refused (`unavailable`) for a file of an\n * encrypted folder. filex 0.56 and older ignore it and write over the file.\n */\n beside?: string;\n /**\n * filex 0.57: `'copy'` - when the file changed since the app opened it\n * (somebody else saved it, a sync client wrote it), the save is written as\n * a conflict copy beside it (`<name>.filex-conflict-<UTC time>.<ext>`)\n * instead of being refused, and `SaveResult.conflict` names it; later saves\n * of the same opening go to that copy. Without it such a save is refused\n * (`failed`, message `changed`), as in 0.56. A file of an encrypted folder\n * is never copied: refused as before.\n */\n conflict?: 'copy';\n}\n\nexport interface SaveResult {\n saved: true;\n size: number;\n /**\n * filex 0.57: the name of the file the save was written to when it is not\n * the opened file - the one beside it (`beside`) or the conflict copy\n * (`conflict`). Absent for a save over the opened file.\n */\n name?: string;\n /** filex 0.57: the save went beside the file, in the format `beside` named. */\n beside?: true;\n /** filex 0.57: the save went to this conflict copy (`conflict: 'copy'`). */\n conflict?: string;\n}\n\n/**\n * `ui.download`: a file for the person's own disk. filex does it (a sandboxed\n * frame cannot download), with the app's `ui:download` grant, on a gesture in\n * the frame or the person's yes, never over LIMITS.maxDownloadBytes.\n */\nexport interface DownloadParams {\n /** The file name offered — a name, not a path. */\n name: string;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface DownloadResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.print` (filex 0.55.0): a PDF for the browser's print dialog. A\n * sandboxed frame may not open that dialog (measured: Chromium ignores\n * `print()` there and says so, Firefox ignores it), so filex prints the PDF\n * from a page of its own, with the app's `ui:print` grant, never over\n * LIMITS.maxPrintBytes. filex asks every time, above the frame, and the\n * dialog opens only on the person's click on filex's Allow. The print dialog\n * can save the PDF too, so `ui:print` is the same kind of grant as\n * `ui:download`. filex 0.54 and older answer `unknown_method`.\n */\nexport interface PrintParams {\n /** The document's name, for the question filex may ask - a name, not a path. */\n name: string;\n /** The PDF: a transferred ArrayBuffer or ReadableStream<Uint8Array>. */\n data: ReadableStream<Uint8Array> | ArrayBuffer;\n /** `application/pdf` when given; nothing else is printed. */\n mime?: string;\n}\n\nexport interface PrintResult {\n /** The browser's print dialog was opened with the PDF. */\n printed: true;\n size: number;\n}\n\n/* ── editing together (coedit.*: filex 0.55 defined, 0.56 offers) ──────── */\n\n/** What a member appends: the editor's changes, a lock request, a release. */\nexport type CoEditKind = 'changes' | 'lock' | 'release';\n\n/** A member of the session. */\nexport interface CoEditMember {\n /** The relay's id for the connection. */\n client: string;\n /** An opaque id of the person, the same for each of their connections in\n * this session; never an account id. */\n user: string;\n /** The person's display name. */\n name: string;\n /** The editor's per-session user index: given once, never twice. */\n indexUser: number;\n /** May append changes (an editor role and the right to change the file). */\n canEdit: boolean;\n}\n\n/** `coedit.join` params: join (or start) the session of an opened file. */\nexport interface CoEditJoinParams {\n index?: number;\n}\n\n/**\n * What `coedit.join` answers: who the app is in the session and how far\n * the log has gone. The entries come as `coedit.entry` events once the app\n * subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`\n * when filex offers no editing together here (filex 0.55, editing together\n * turned off, an encrypted file, a vault): the app edits alone - the message\n * says why (`not_ready` after waiting for a session's creator, `encrypted`,\n * `nothing_to_watch` for a person who may only read the file while nobody\n * edits it, or the server's code: `coedit_off`, `coedit_build`,\n * `coedit_file_changed`).\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /**\n * The last entry's seq when filex handed the session over (0: an empty\n * log). filex 0.56: the changes this browser kept from an earlier opening\n * of the file (a lost connection, a closed tab), which filex sends before\n * it answers, are in the log up to here - an editor that starts once it\n * has read this far opens with them.\n */\n head: number;\n /** The last `changes` entry's seq, as far as `head`. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\n /**\n * filex 0.56: this member started the session. Before anybody else can\n * join it puts the base document - EXACTLY the bytes it opened - with\n * `coedit.blob.put('base', bytes)`. A member that did not start it opens\n * `coedit.blob.get('base')`, never the file: the file may hold a save of\n * the session already, and the log applies to the base.\n */\n created: boolean;\n}\n\n/** `coedit.dropped` event data (filex 0.56). */\nexport interface CoEditDropped {\n /** The last entry the host handed over. */\n from: number;\n /** `gone`: the membership ended (the session closed, or the page was away\n * too long) - join again. `broken`: an entry did not check out (missing,\n * moved, altered, replayed) - stop editing together. */\n reason: 'gone' | 'broken';\n}\n\n/** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */\nexport interface CoEditSubscribeParams {\n from: number;\n}\n\n/** `coedit.append` params: the host seals it and the relay places it. */\nexport interface CoEditAppendParams {\n kind: CoEditKind;\n body: unknown;\n}\n\nexport interface CoEditAppendResult {\n /** Where it landed; it comes back as a `coedit.entry` too. */\n seq: number;\n}\n\n/** `coedit.lease`: the changes lease (one writer at a time, one that has\n * seen every change). */\nexport interface CoEditLeaseParams {\n op: 'acquire' | 'release';\n /** The seq of the last `changes` entry the app applied. */\n changesSeen: number;\n}\n\nexport interface CoEditLeaseResult {\n granted: boolean;\n}\n\n/** `coedit.cursor` params: passed to the other members, not kept. The host\n * sends a few a second at most (the last one wins). */\nexport interface CoEditCursorParams {\n cursor: unknown;\n}\n\n/** `coedit.blob.put` / `coedit.blob.get`: the session's sealed blobs (the\n * base document, images added while editing). A name, not a path. */\nexport interface CoEditBlobPutParams {\n name: string;\n data: ArrayBuffer;\n}\n\nexport interface CoEditBlobGetParams {\n name: string;\n}\n\nexport interface CoEditBlobGetResult {\n name: string;\n bytes: ArrayBuffer;\n}\n\ninterface CoEditEntryBase {\n seq: number;\n /** The relay's time, Unix ms - the same for every member. */\n at: number;\n client: string;\n}\n\n/** One entry of the session's log, opened and checked by filex. */\nexport type CoEditEntry =\n | (CoEditEntryBase & { kind: CoEditKind; body: unknown })\n | (CoEditEntryBase & { kind: 'join'; member: CoEditMember })\n | (CoEditEntryBase & { kind: 'leave' })\n | (CoEditEntryBase & { kind: 'saved'; through: number });\n\n/** `coedit.cursor` event data. */\nexport interface CoEditCursor {\n client: string;\n cursor: unknown;\n}\n\n/**\n * `file.saveAs`: a NEW file, in a folder the person picks in filex's own\n * folder dialog (the one its Move to… uses), which opens in the opened\n * file's folder. Closing the dialog answers `cancelled`.\n */\nexport interface SaveAsParams {\n /** The file name — a name, not a path. */\n name: string;\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveAsResult {\n saved: true;\n name: string;\n size: number;\n}\n\nexport interface ToastParams {\n text: string;\n tone?: 'info' | 'success' | 'warning' | 'error';\n}\n\nexport interface ConfirmParams {\n title?: string;\n text: string;\n confirm?: string;\n cancel?: string;\n danger?: boolean;\n}\n\n/**\n * `license.get` (filex 0.52.0): what the app's license says. A paid app is\n * installed from a store, which issues its license and is asked about it every\n * day; filex keeps the key and never hands it to the app. `status`:\n *\n * - `free`: not a paid app;\n * - `valid`: the store said valid at its last check;\n * - `grace`: valid, but the store could not be asked since - it holds until\n * the grace the store signed ends;\n * - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,\n * `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held\n * and its interface is not served, so an app rarely reads one of these.\n *\n * `valid_until` / `updates_until` are there for a valid license (dates as\n * RFC 3339). Who holds the license is not the app's to read. filex 0.51.0\n * and older answer `unknown_method`.\n */\nexport interface LicenseInfo {\n status: string;\n valid_until?: string;\n updates_until?: string;\n}\n\n/** `engine.call`: the app's own module, `ui_call` export. */\nexport interface EngineCallParams {\n method: string;\n params?: unknown;\n}\n\n/** `job.submit`: queue one of the app's actions on the opened files. */\nexport interface JobSubmitParams {\n action: string;\n params?: Record<string, unknown>;\n}\n\n/* ── limits ────────────────────────────────────────────────────────────── */\n\nexport const LIMITS = {\n /** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */\n maxTextBytes: 32 << 20,\n /** One `state.set` value, as JSON. */\n maxStateBytes: 8 << 10,\n /** A `ui.toast` / `ui.title` text, in characters. */\n maxLineChars: 300,\n /** How long the SDK waits for the host's port. */\n connectTimeoutMs: 10_000,\n /** The largest file `ui.download` hands the person (streamed or not). */\n maxDownloadBytes: 256 << 20,\n /**\n * The largest PDF `ui.print` prints (filex 0.55). Lower than a download:\n * a download can stream to disk, a print is held whole in the page (one\n * Blob) until the print dialog has read it, and a document worth printing\n * is far smaller.\n */\n maxPrintBytes: 64 << 20,\n /**\n * The largest file of an encrypted folder filex hands an app in the clear\n * or takes back from it (filex 0.56, `FileInfo.plaintext`): it is\n * decrypted and encrypted whole in the page, as filex's own explorer does\n * for a file of this size.\n */\n maxEncryptedBytes: 200 << 20,\n} as const;\n\n/* ── guards ────────────────────────────────────────────────────────────── */\n\nexport function isHello(d: unknown): d is HelloMessage {\n return !!d && typeof d === 'object' && (d as HelloMessage).type === HELLO && typeof (d as HelloMessage).v === 'number';\n}\n\nexport function isPortMessage(d: unknown): d is PortMessage {\n return !!d && typeof d === 'object' && (d as PortMessage).type === PORT;\n}\n\nexport function isRequest(d: unknown): d is RequestMessage {\n return (\n !!d &&\n typeof d === 'object' &&\n typeof (d as RequestMessage).id === 'number' &&\n typeof (d as RequestMessage).method === 'string'\n );\n}\n\nexport function isKnownMethod(m: string): m is Method {\n return (METHODS as readonly string[]).includes(m);\n}\n","/**\n * @brftech/filex-app-ui — the SDK an app's own interface bundles.\n *\n * ```js\n * import { connect } from '@brftech/filex-app-ui';\n *\n * const fx = await connect(); // the handshake with filex\n * const file = await fx.open(); // the file the app was opened with\n * editor.setValue(await file.text());\n * editor.onChange(() => fx.dirty(true));\n * fx.onSave(async () => editor.getValue()); // filex's Save, a draft's \"Save to disk\", Ctrl+S\n * ```\n *\n * No dependency, a few KB. The interface runs in a sandboxed frame with no\n * network and no storage of its own: every call below goes to filex over the\n * MessagePort the handshake set up, and filex decides it (docs/APP-PLUGINS.md\n * → \"An app's own interface\"). The protocol itself is `./protocol`.\n */\nimport {\n BRIDGE_VERSION,\n HELLO,\n LIMITS,\n isPortMessage,\n type BridgeError,\n type ConfirmParams,\n type ErrorCode,\n type FileInfo,\n type HostEvent,\n type HostRequestMessage,\n type Method,\n type ReadResult,\n type SaveAsResult,\n type SaveResult,\n type Session,\n type Theme,\n type ToastParams,\n type DownloadResult,\n type LicenseInfo,\n type PrintResult,\n type CoEditHello,\n type CoEditKind,\n type CoEditAppendResult,\n type CoEditLeaseResult,\n type CoEditBlobGetResult,\n type CsvDeclaration,\n} from './protocol';\n\nexport * from './protocol';\n\n/** A call filex refused, with the reason as a code. */\nexport class FilexError extends Error {\n readonly code: ErrorCode;\n constructor(err: BridgeError) {\n super(err.message || err.code);\n this.name = 'FilexError';\n this.code = err.code;\n }\n}\n\n/** Content the app hands to a save. */\nexport type SaveData = string | Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>;\n\n/** One opened file, read the way the app needs it. */\nexport interface OpenedFile extends FileInfo {\n text(): Promise<string>;\n bytes(): Promise<ArrayBuffer>;\n stream(): Promise<ReadableStream<Uint8Array>>;\n /**\n * The whole answer of a read (filex 0.57): the content `as` asks for, and\n * what filex knows about how the file is written (`text_dialect`, for a\n * `.csv`).\n */\n read(as?: 'stream' | 'bytes' | 'text'): Promise<ReadResult>;\n /** Save new content over this file (a new version, or the draft it is). */\n save(data: SaveData, mime?: string): Promise<SaveResult>;\n}\n\nexport interface ConnectOptions {\n /** How long to wait for filex (default 10 s). */\n timeoutMs?: number;\n /**\n * Put filex's colours (`--fe-*`), `lang`, `dir` and `data-theme` on\n * `<html>`, and keep them current (default true).\n */\n applyTheme?: boolean;\n /** Ctrl/Cmd+S inside the interface runs the save handler (default true). */\n saveShortcut?: boolean;\n}\n\nexport interface FilexApp {\n readonly session: Session;\n /** The file at `index` (default the first) the interface was opened with. */\n open(index?: number): Promise<OpenedFile>;\n /**\n * Save new content over an opened file. `through` (filex 0.56, editing\n * together): the last log entry the saved document holds, so the session\n * knows what is saved. `csv` (filex 0.57): how the CSV saved over a `.csv`\n * is written (CsvDeclaration); filex checks it against the bytes and\n * writes the file in its own way. `beside` (filex 0.57): the save is in\n * this format, which the file cannot hold - written beside it under a name\n * filex chooses (`SaveResult.name`). `conflict: 'copy'` (filex 0.57): a\n * file that changed since it was opened gets the save as a conflict copy\n * (`SaveResult.conflict`) instead of a refusal.\n */\n save(\n data: SaveData,\n opts?: { index?: number; mime?: string; through?: number; csv?: CsvDeclaration; beside?: string; conflict?: 'copy' },\n ): Promise<SaveResult>;\n /** Save a NEW file; filex asks the person where (its own folder picker). */\n saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;\n /** Unsaved changes, or not: filex asks before the person leaves them. */\n dirty(on: boolean): void;\n title(text: string): void;\n toast(text: string, tone?: ToastParams['tone']): void;\n confirm(opts: ConfirmParams | string): Promise<boolean>;\n /** Ask filex to close the interface (it asks first when there are changes). */\n close(): void;\n /** Put text on the clipboard (filex does it, the same way in every browser). */\n copy(text: string): Promise<void>;\n /**\n * Hand the person a file for their own disk (the app needs `ui.download`\n * in its manifest). Call it from a click or a key press: without one,\n * filex asks the person first. `cancelled` when they say no.\n */\n download(name: string, data: SaveData, mime?: string): Promise<DownloadResult>;\n /**\n * Print a PDF (filex 0.55; the app needs `ui.print` in its manifest): a\n * sandboxed frame may not open the print dialog, so filex prints it from\n * its own page. filex asks the person every time, and the dialog opens on\n * their click on filex's Allow; at most LIMITS.maxPrintBytes. filex 0.54\n * and older answer `unknown_method`, and a host whose print page may not\n * be shown `unavailable`: hand the PDF over with `download` then.\n */\n print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;\n /**\n * Editing together (filex 0.56; the app's manifest says `\"co_edit\":\n * {\"open\": true}`, granted as `files:co-edit`). filex seals what the app appends, orders it\n * through its relay and hands every member the same entries in the same\n * order (`coedit.entry` events after `subscribe`); the app never holds a\n * key. `join` answering `unavailable` (filex 0.55, editing together off\n * here, an encrypted file) means: edit alone. The member that started the\n * session (`created`) puts the base document before anybody else may join.\n */\n coedit: {\n join(index?: number): Promise<CoEditHello>;\n subscribe(from: number): Promise<void>;\n append(kind: CoEditKind, body: unknown): Promise<CoEditAppendResult>;\n lease(op: 'acquire' | 'release', changesSeen: number): Promise<boolean>;\n cursor(cursor: unknown): void;\n putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView): Promise<void>;\n getBlob(name: string): Promise<ArrayBuffer>;\n leave(): Promise<void>;\n };\n /** Call the app's own module (`ui_call` export) with the opened files. */\n call<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Queue one of the app's actions on the opened files; answers the op. */\n submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;\n /**\n * This person's small store for this app (JSON values, 8 KiB each). While\n * the app holds a file of an encrypted folder in the clear\n * (`FileInfo.plaintext`, filex 0.56) it is another store, sealed in the\n * browser - apart from this one, and empty while the folder is locked.\n */\n state: {\n get<T = unknown>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n };\n /**\n * The app's license (filex 0.52.0; a paid app installed from a store):\n * `{status: \"free\"}` for a free app, `{status: \"valid\", valid_until,\n * updates_until}` for a licensed one. Never the key, never the licensee.\n */\n license: {\n get(): Promise<LicenseInfo>;\n };\n /** Listen to filex. Returns the unsubscribe. */\n on(event: HostEvent, handler: (data: unknown) => void): () => void;\n /**\n * What to save when filex asks (its Save button, a draft's \"Save to disk\",\n * Ctrl+S): return the document and the SDK saves it over the opened file;\n * return nothing if the handler saved by itself.\n */\n onSave(handler: () => SaveData | void | Promise<SaveData | void>): () => void;\n /** Raw call, for a method this SDK has no helper for. */\n request<T = unknown>(method: Method, params?: unknown, transfer?: Transferable[]): Promise<T>;\n}\n\nlet connecting: Promise<FilexApp> | null = null;\n\n/**\n * The handshake. Resolves once filex answered with its port and the session;\n * rejects outside filex or when filex did not answer in time. Calling it again\n * answers the same connection.\n */\nexport function connect(opts: ConnectOptions = {}): Promise<FilexApp> {\n if (!connecting) connecting = open(opts);\n return connecting;\n}\n\nfunction open(opts: ConnectOptions): Promise<FilexApp> {\n return new Promise<FilexApp>((resolve, reject) => {\n if (typeof window === 'undefined' || window.parent === window) {\n reject(new FilexError({ code: 'unavailable', message: 'not running inside filex' }));\n return;\n }\n const parent = window.parent;\n // Where filex runs, when the browser says so (Chrome, Safari). A frame\n // drawn with referrerpolicy=\"no-referrer\" has no document.referrer, and\n // Firefox has no ancestorOrigins: then `source === parent` is the check,\n // which is the one that matters (only the parent window IS the parent).\n const expected = ancestorOrigin();\n const timer = setTimeout(() => {\n window.removeEventListener('message', onMessage);\n reject(new FilexError({ code: 'unavailable', message: 'filex did not answer' }));\n }, opts.timeoutMs ?? LIMITS.connectTimeoutMs);\n\n function onMessage(ev: MessageEvent) {\n if (ev.source !== parent || !isPortMessage(ev.data) || !ev.ports[0]) return;\n if (expected && ev.origin !== expected) return;\n window.removeEventListener('message', onMessage);\n clearTimeout(timer);\n const port = ev.ports[0];\n const app = bridge(port, opts);\n app\n .request<Session>('session.get')\n .then((session) => {\n (app as { session: Session }).session = session;\n if (opts.applyTheme !== false) {\n applyLook(session.theme, session.locale, session.dir);\n app.on('theme', (t) => applyLook(t as Theme));\n app.on('locale', (l) => {\n const v = l as { locale?: string; dir?: 'ltr' | 'rtl' };\n applyLook(undefined, v.locale, v.dir);\n });\n }\n resolve(app);\n })\n .catch(reject);\n }\n window.addEventListener('message', onMessage);\n parent.postMessage({ type: HELLO, v: BRIDGE_VERSION }, '*');\n });\n}\n\nfunction ancestorOrigin(): string {\n try {\n const list = (window.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins;\n if (list && list.length > 0 && list[0] !== 'null') return list[0];\n } catch {\n /* no such API */\n }\n return '';\n}\n\nfunction bridge(port: MessagePort, opts: ConnectOptions): FilexApp {\n let seq = 0;\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();\n const listeners = new Map<HostEvent, Set<(data: unknown) => void>>();\n let saveHandler: (() => SaveData | void | Promise<SaveData | void>) | null = null;\n\n port.onmessage = (ev: MessageEvent) => {\n const m = ev.data as Record<string, unknown> | null;\n if (!m || typeof m !== 'object') return;\n if (typeof m.id === 'number') {\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n if (m.error) p.reject(new FilexError(m.error as BridgeError));\n else p.resolve(m.result);\n return;\n }\n if (typeof m.hid === 'number') {\n void answerHost(m as unknown as HostRequestMessage);\n return;\n }\n if (typeof m.event === 'string') {\n for (const h of listeners.get(m.event as HostEvent) ?? []) {\n try {\n h(m.data);\n } catch (e) {\n console.error('[filex-app-ui] listener failed', e);\n }\n }\n }\n };\n\n async function answerHost(m: HostRequestMessage) {\n if (m.request === 'save') {\n if (!saveHandler) {\n port.postMessage({ hid: m.hid, error: { code: 'unavailable', message: 'this app has no save handler' } });\n return;\n }\n try {\n const res = await runSave();\n port.postMessage({ hid: m.hid, result: res ?? { saved: true } });\n } catch (e) {\n const err = e instanceof FilexError ? { code: e.code, message: e.message } : { code: 'failed', message: String((e as Error)?.message ?? e) };\n port.postMessage({ hid: m.hid, error: err });\n }\n return;\n }\n port.postMessage({ hid: m.hid, error: { code: 'unknown_method' } });\n }\n\n async function runSave(): Promise<SaveResult | null> {\n if (!saveHandler) return null;\n const data = await saveHandler();\n if (data === undefined || data === null) return null;\n return app.save(data as SaveData);\n }\n\n function request<T>(method: Method, params?: unknown, transfer: Transferable[] = []): Promise<T> {\n const id = ++seq;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject });\n try {\n port.postMessage({ id, method, params }, transfer);\n } catch (e) {\n pending.delete(id);\n reject(e);\n }\n });\n }\n\n function notify(method: Method, params?: unknown) {\n request(method, params).catch((e) => console.warn('[filex-app-ui]', method, e));\n }\n\n async function payload(data: SaveData): Promise<{ data: ReadableStream<Uint8Array> | ArrayBuffer | string; transfer: Transferable[] }> {\n if (typeof data === 'string') return { data, transfer: [] };\n if (data instanceof ArrayBuffer) return { data, transfer: [data] };\n if (ArrayBuffer.isView(data)) {\n const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n return { data: copy, transfer: [copy] };\n }\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n const buf = await data.arrayBuffer();\n return { data: buf, transfer: [buf] };\n }\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {\n return { data, transfer: [data as unknown as Transferable] };\n }\n throw new FilexError({ code: 'invalid', message: 'save takes a string, a Blob, bytes or a ReadableStream' });\n }\n\n const app: FilexApp = {\n session: undefined as unknown as Session,\n async open(index = 0) {\n const info = app.session?.files?.[index];\n if (!info) throw new FilexError({ code: 'not_found', message: `no file at ${index}` });\n const read = <K extends keyof ReadResult>(as: 'stream' | 'bytes' | 'text', key: K) =>\n request<ReadResult>('file.read', { index, as }).then((r) => r[key] as NonNullable<ReadResult[K]>);\n return {\n ...info,\n text: () => read('text', 'text'),\n bytes: () => read('bytes', 'bytes'),\n stream: () => read('stream', 'stream'),\n read: (as: 'stream' | 'bytes' | 'text' = 'bytes') => request<ReadResult>('file.read', { index, as }),\n save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),\n };\n },\n async save(data, o = {}) {\n const p = await payload(data);\n const params: Record<string, unknown> = { index: o.index ?? 0, data: p.data, mime: o.mime };\n if (typeof o.through === 'number') params.through = o.through;\n if (o.csv) params.csv = { ...o.csv };\n if (o.beside) params.beside = o.beside;\n if (o.conflict) params.conflict = o.conflict;\n return request<SaveResult>('file.save', params, p.transfer);\n },\n async saveAs(name, data, mime) {\n const p = await payload(data);\n return request<SaveAsResult>('file.saveAs', { name, data: p.data, mime }, p.transfer);\n },\n async download(name, data, mime) {\n const p = await payload(data);\n return request<DownloadResult>('ui.download', { name, data: p.data, mime }, p.transfer);\n },\n async print(name, pdf) {\n const p = await payload(pdf);\n return request<PrintResult>('ui.print', { name, data: p.data, mime: 'application/pdf' }, p.transfer);\n },\n coedit: {\n join(index = 0) {\n return request<CoEditHello>('coedit.join', { index });\n },\n subscribe(from: number) {\n return request<void>('coedit.subscribe', { from });\n },\n append(kind: CoEditKind, body: unknown) {\n return request<CoEditAppendResult>('coedit.append', { kind, body });\n },\n async lease(op: 'acquire' | 'release', changesSeen: number) {\n const r = await request<CoEditLeaseResult>('coedit.lease', { op, changesSeen });\n return !!r?.granted;\n },\n cursor(cursor: unknown) {\n notify('coedit.cursor', { cursor });\n },\n async putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView) {\n const p = await payload(data);\n if (!(p.data instanceof ArrayBuffer)) throw new FilexError({ code: 'invalid', message: 'a blob is bytes' });\n await request<void>('coedit.blob.put', { name, data: p.data }, p.transfer);\n },\n async getBlob(name: string) {\n const r = await request<CoEditBlobGetResult>('coedit.blob.get', { name });\n return r.bytes;\n },\n async leave() {\n await request<void>('coedit.leave');\n },\n },\n dirty(on) {\n notify('ui.dirty', { dirty: !!on });\n },\n title(text) {\n notify('ui.title', { text: String(text).slice(0, LIMITS.maxLineChars) });\n },\n toast(text, tone) {\n notify('ui.toast', { text: String(text).slice(0, LIMITS.maxLineChars), tone });\n },\n confirm(o) {\n const params = typeof o === 'string' ? { text: o } : o;\n return request<boolean>('ui.confirm', params);\n },\n close() {\n notify('ui.close');\n },\n copy(text) {\n return request<void>('clipboard.write', { text: String(text) });\n },\n call<T>(method: string, params?: unknown) {\n return request<T>('engine.call', { method, params });\n },\n submit(action, params) {\n return request<{ op: unknown }>('job.submit', { action, params });\n },\n state: {\n get<T>(key: string) {\n return request<T | undefined>('state.get', { key });\n },\n set(key: string, value: unknown) {\n return request<void>('state.set', { key, value });\n },\n },\n license: {\n get() {\n return request<LicenseInfo>('license.get');\n },\n },\n on(event, handler) {\n let set = listeners.get(event);\n if (!set) listeners.set(event, (set = new Set()));\n set.add(handler);\n return () => set!.delete(handler);\n },\n onSave(handler) {\n saveHandler = handler;\n return () => {\n if (saveHandler === handler) saveHandler = null;\n };\n },\n request,\n };\n\n if (opts.saveShortcut !== false && typeof document !== 'undefined') {\n // ⚠ The keystroke is handled HERE and never forwarded: a frame that could\n // send key events to filex could press filex's own shortcuts. Ctrl+S runs\n // the app's own save handler, which saves through the bridge like any call.\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 's' || e.key === 'S') && saveHandler) {\n e.preventDefault();\n runSave().catch((err) => app.toast(String((err as Error)?.message ?? err), 'error'));\n }\n },\n true,\n );\n }\n return app;\n}\n\n/** Paint filex's look onto the interface's own document. */\nfunction applyLook(theme?: Theme, locale?: string, dir?: 'ltr' | 'rtl') {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (theme) {\n root.dataset.theme = theme.mode;\n root.style.colorScheme = theme.mode;\n for (const [k, v] of Object.entries(theme.tokens ?? {})) {\n if (/^--fe-[a-z0-9-]+$/.test(k) && typeof v === 'string') root.style.setProperty(k, v);\n }\n }\n if (locale) root.lang = locale;\n if (dir) root.dir = dir;\n}\n"],"names":["BRIDGE_VERSION","HELLO","PORT","METHODS","LIMITS","isHello","d","isPortMessage","isRequest","isKnownMethod","m","FilexError","err","connecting","connect","opts","open","resolve","reject","parent","expected","ancestorOrigin","timer","onMessage","ev","port","app","bridge","session","applyLook","t","l","v","list","seq","pending","listeners","saveHandler","p","answerHost","h","e","res","runSave","data","request","method","params","transfer","id","notify","payload","copy","buf","index","info","read","as","key","r","mime","o","name","pdf","from","kind","body","op","changesSeen","cursor","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -179,6 +179,26 @@ export declare interface ConnectOptions {
|
|
|
179
179
|
saveShortcut?: boolean;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* What an app says about the CSV it saves (filex 0.57; `SaveParams.csv`):
|
|
184
|
+
* how ITS bytes are written. filex reads the same from the bytes and refuses
|
|
185
|
+
* a save they contradict (`invalid`, message `csv_dialect`): nothing is
|
|
186
|
+
* written. Whatever the app wrote, the file is written in its own way - its
|
|
187
|
+
* delimiter, byte order mark, line ends and encoding - with every cell nobody
|
|
188
|
+
* changed as the file has it. A field left out is not checked; no `csv` at
|
|
189
|
+
* all is read from the bytes alone.
|
|
190
|
+
*/
|
|
191
|
+
export declare interface CsvDeclaration {
|
|
192
|
+
/** One character: `,`, `;`, `\t` or `|`. */
|
|
193
|
+
delimiter?: string;
|
|
194
|
+
/** As TextDialect.encoding names it (`utf-8` for a string the SDK sent). */
|
|
195
|
+
encoding?: string;
|
|
196
|
+
/** The bytes start with a byte order mark. */
|
|
197
|
+
bom?: boolean;
|
|
198
|
+
/** Their line ends. */
|
|
199
|
+
eol?: 'crlf' | 'lf';
|
|
200
|
+
}
|
|
201
|
+
|
|
182
202
|
/**
|
|
183
203
|
* `ui.download`: a file for the person's own disk. filex does it (a sandboxed
|
|
184
204
|
* frame cannot download), with the app's `ui:download` grant, on a gesture in
|
|
@@ -290,12 +310,21 @@ export declare interface FilexApp {
|
|
|
290
310
|
/**
|
|
291
311
|
* Save new content over an opened file. `through` (filex 0.56, editing
|
|
292
312
|
* together): the last log entry the saved document holds, so the session
|
|
293
|
-
* knows what is saved.
|
|
313
|
+
* knows what is saved. `csv` (filex 0.57): how the CSV saved over a `.csv`
|
|
314
|
+
* is written (CsvDeclaration); filex checks it against the bytes and
|
|
315
|
+
* writes the file in its own way. `beside` (filex 0.57): the save is in
|
|
316
|
+
* this format, which the file cannot hold - written beside it under a name
|
|
317
|
+
* filex chooses (`SaveResult.name`). `conflict: 'copy'` (filex 0.57): a
|
|
318
|
+
* file that changed since it was opened gets the save as a conflict copy
|
|
319
|
+
* (`SaveResult.conflict`) instead of a refusal.
|
|
294
320
|
*/
|
|
295
321
|
save(data: SaveData, opts?: {
|
|
296
322
|
index?: number;
|
|
297
323
|
mime?: string;
|
|
298
324
|
through?: number;
|
|
325
|
+
csv?: CsvDeclaration;
|
|
326
|
+
beside?: string;
|
|
327
|
+
conflict?: 'copy';
|
|
299
328
|
}): Promise<SaveResult>;
|
|
300
329
|
/** Save a NEW file; filex asks the person where (its own folder picker). */
|
|
301
330
|
saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;
|
|
@@ -519,6 +548,12 @@ export declare interface OpenedFile extends FileInfo {
|
|
|
519
548
|
text(): Promise<string>;
|
|
520
549
|
bytes(): Promise<ArrayBuffer>;
|
|
521
550
|
stream(): Promise<ReadableStream<Uint8Array>>;
|
|
551
|
+
/**
|
|
552
|
+
* The whole answer of a read (filex 0.57): the content `as` asks for, and
|
|
553
|
+
* what filex knows about how the file is written (`text_dialect`, for a
|
|
554
|
+
* `.csv`).
|
|
555
|
+
*/
|
|
556
|
+
read(as?: 'stream' | 'bytes' | 'text'): Promise<ReadResult>;
|
|
522
557
|
/** Save new content over this file (a new version, or the draft it is). */
|
|
523
558
|
save(data: SaveData, mime?: string): Promise<SaveResult>;
|
|
524
559
|
}
|
|
@@ -571,6 +606,13 @@ export declare interface ReadResult {
|
|
|
571
606
|
stream?: ReadableStream<Uint8Array>;
|
|
572
607
|
bytes?: ArrayBuffer;
|
|
573
608
|
text?: string;
|
|
609
|
+
/**
|
|
610
|
+
* filex 0.57: how a `.csv` is written (its delimiter, byte order mark,
|
|
611
|
+
* line ends, encoding). Absent for any other file, for a file of an
|
|
612
|
+
* encrypted folder (the server holds its ciphertext only), and from filex
|
|
613
|
+
* 0.56 and older.
|
|
614
|
+
*/
|
|
615
|
+
text_dialect?: TextDialect;
|
|
574
616
|
}
|
|
575
617
|
|
|
576
618
|
export declare interface RequestMessage {
|
|
@@ -618,11 +660,50 @@ export declare interface SaveParams {
|
|
|
618
660
|
* outside a session.
|
|
619
661
|
*/
|
|
620
662
|
through?: number;
|
|
663
|
+
/**
|
|
664
|
+
* filex 0.57: what the app says about the CSV it saves over a `.csv`
|
|
665
|
+
* (CsvDeclaration). Ignored for any other file and for a file of an
|
|
666
|
+
* encrypted folder; filex 0.56 and older ignore it.
|
|
667
|
+
*/
|
|
668
|
+
csv?: CsvDeclaration;
|
|
669
|
+
/**
|
|
670
|
+
* filex 0.57: the save is in this format (an extension: `docx`, `xlsx`,
|
|
671
|
+
* `pptx`, `docm`, `xlsm`, `pptm`, `odt`, `ods`, `odp`), which the opened
|
|
672
|
+
* file cannot hold (a `.doc`, `.xls`, `.ppt`...). It is written BESIDE the
|
|
673
|
+
* file, which is left as it is, under a name filex chooses - the file's own
|
|
674
|
+
* name with that extension, `rapor (2).docx` when that is taken - and only
|
|
675
|
+
* for a person who may create files in that folder (`permission_denied`
|
|
676
|
+
* otherwise). `SaveResult.name` says where; later saves of the same
|
|
677
|
+
* opening go to that file. The same extension as the file's own: an
|
|
678
|
+
* ordinary save over it. Refused (`unavailable`) for a file of an
|
|
679
|
+
* encrypted folder. filex 0.56 and older ignore it and write over the file.
|
|
680
|
+
*/
|
|
681
|
+
beside?: string;
|
|
682
|
+
/**
|
|
683
|
+
* filex 0.57: `'copy'` - when the file changed since the app opened it
|
|
684
|
+
* (somebody else saved it, a sync client wrote it), the save is written as
|
|
685
|
+
* a conflict copy beside it (`<name>.filex-conflict-<UTC time>.<ext>`)
|
|
686
|
+
* instead of being refused, and `SaveResult.conflict` names it; later saves
|
|
687
|
+
* of the same opening go to that copy. Without it such a save is refused
|
|
688
|
+
* (`failed`, message `changed`), as in 0.56. A file of an encrypted folder
|
|
689
|
+
* is never copied: refused as before.
|
|
690
|
+
*/
|
|
691
|
+
conflict?: 'copy';
|
|
621
692
|
}
|
|
622
693
|
|
|
623
694
|
export declare interface SaveResult {
|
|
624
695
|
saved: true;
|
|
625
696
|
size: number;
|
|
697
|
+
/**
|
|
698
|
+
* filex 0.57: the name of the file the save was written to when it is not
|
|
699
|
+
* the opened file - the one beside it (`beside`) or the conflict copy
|
|
700
|
+
* (`conflict`). Absent for a save over the opened file.
|
|
701
|
+
*/
|
|
702
|
+
name?: string;
|
|
703
|
+
/** filex 0.57: the save went beside the file, in the format `beside` named. */
|
|
704
|
+
beside?: true;
|
|
705
|
+
/** filex 0.57: the save went to this conflict copy (`conflict: 'copy'`). */
|
|
706
|
+
conflict?: string;
|
|
626
707
|
}
|
|
627
708
|
|
|
628
709
|
/** What `session.get` answers — what the app knows about where it runs. */
|
|
@@ -640,10 +721,24 @@ export declare interface Session {
|
|
|
640
721
|
locale: string;
|
|
641
722
|
dir: 'ltr' | 'rtl';
|
|
642
723
|
theme: Theme;
|
|
643
|
-
/**
|
|
724
|
+
/**
|
|
725
|
+
* The person's display name (their profile's, else their username); never
|
|
726
|
+
* their e-mail, never a token. '' where nobody is signed in (a public
|
|
727
|
+
* page). An editor writes it as the author of comments and tracked
|
|
728
|
+
* changes.
|
|
729
|
+
*/
|
|
644
730
|
user: {
|
|
645
731
|
name: string;
|
|
646
732
|
};
|
|
733
|
+
/**
|
|
734
|
+
* filex 0.57: the language the administrator fixed for document editors
|
|
735
|
+
* (Admin → External services → ONLYOFFICE → Editor language,
|
|
736
|
+
* `FILEX_ONLYOFFICE_LANG`; a code such as `de`, `pt-PT`, `zh-TW`). An app
|
|
737
|
+
* that draws a document editor draws it in this language rather than the
|
|
738
|
+
* reader's `locale`; its own words stay in `locale`. Absent: each person's
|
|
739
|
+
* own language decides.
|
|
740
|
+
*/
|
|
741
|
+
editor_lang?: string;
|
|
647
742
|
files: FileInfo[];
|
|
648
743
|
/** The permissions this app was granted — so it can hide what it may not do. */
|
|
649
744
|
grants: string[];
|
|
@@ -651,6 +746,26 @@ export declare interface Session {
|
|
|
651
746
|
settings?: Record<string, string>;
|
|
652
747
|
}
|
|
653
748
|
|
|
749
|
+
/**
|
|
750
|
+
* How a CSV file is written (filex 0.57): `ReadResult.text_dialect` of a
|
|
751
|
+
* `.csv`, read by the server from the file's own bytes.
|
|
752
|
+
*/
|
|
753
|
+
export declare interface TextDialect {
|
|
754
|
+
/** One character: `,`, `;`, `\t` or `|`. */
|
|
755
|
+
delimiter: string;
|
|
756
|
+
/** The file starts with a byte order mark. */
|
|
757
|
+
bom: boolean;
|
|
758
|
+
/** Its lines end in CRLF (else LF). */
|
|
759
|
+
crlf: boolean;
|
|
760
|
+
/**
|
|
761
|
+
* Its encoding as a browser's TextDecoder names it: `utf-8`, `utf-16le`,
|
|
762
|
+
* `utf-16be`, or a code page (`windows-1250`, `windows-1251`,
|
|
763
|
+
* `windows-1252`, `windows-1254`). `file.read` with `as: 'text'` decodes
|
|
764
|
+
* with it; `bytes` and `stream` hand the bytes as they are.
|
|
765
|
+
*/
|
|
766
|
+
encoding: string;
|
|
767
|
+
}
|
|
768
|
+
|
|
654
769
|
/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */
|
|
655
770
|
export declare interface Theme {
|
|
656
771
|
mode: 'light' | 'dark';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-app-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "filex app interface SDK: the bridge between an app's own sandboxed interface and filex (open, save, dirty, call the app's module)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/filex-app-ui.js",
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type CoEditAppendResult,
|
|
43
43
|
type CoEditLeaseResult,
|
|
44
44
|
type CoEditBlobGetResult,
|
|
45
|
+
type CsvDeclaration,
|
|
45
46
|
} from './protocol';
|
|
46
47
|
|
|
47
48
|
export * from './protocol';
|
|
@@ -64,6 +65,12 @@ export interface OpenedFile extends FileInfo {
|
|
|
64
65
|
text(): Promise<string>;
|
|
65
66
|
bytes(): Promise<ArrayBuffer>;
|
|
66
67
|
stream(): Promise<ReadableStream<Uint8Array>>;
|
|
68
|
+
/**
|
|
69
|
+
* The whole answer of a read (filex 0.57): the content `as` asks for, and
|
|
70
|
+
* what filex knows about how the file is written (`text_dialect`, for a
|
|
71
|
+
* `.csv`).
|
|
72
|
+
*/
|
|
73
|
+
read(as?: 'stream' | 'bytes' | 'text'): Promise<ReadResult>;
|
|
67
74
|
/** Save new content over this file (a new version, or the draft it is). */
|
|
68
75
|
save(data: SaveData, mime?: string): Promise<SaveResult>;
|
|
69
76
|
}
|
|
@@ -87,9 +94,18 @@ export interface FilexApp {
|
|
|
87
94
|
/**
|
|
88
95
|
* Save new content over an opened file. `through` (filex 0.56, editing
|
|
89
96
|
* together): the last log entry the saved document holds, so the session
|
|
90
|
-
* knows what is saved.
|
|
97
|
+
* knows what is saved. `csv` (filex 0.57): how the CSV saved over a `.csv`
|
|
98
|
+
* is written (CsvDeclaration); filex checks it against the bytes and
|
|
99
|
+
* writes the file in its own way. `beside` (filex 0.57): the save is in
|
|
100
|
+
* this format, which the file cannot hold - written beside it under a name
|
|
101
|
+
* filex chooses (`SaveResult.name`). `conflict: 'copy'` (filex 0.57): a
|
|
102
|
+
* file that changed since it was opened gets the save as a conflict copy
|
|
103
|
+
* (`SaveResult.conflict`) instead of a refusal.
|
|
91
104
|
*/
|
|
92
|
-
save(
|
|
105
|
+
save(
|
|
106
|
+
data: SaveData,
|
|
107
|
+
opts?: { index?: number; mime?: string; through?: number; csv?: CsvDeclaration; beside?: string; conflict?: 'copy' },
|
|
108
|
+
): Promise<SaveResult>;
|
|
93
109
|
/** Save a NEW file; filex asks the person where (its own folder picker). */
|
|
94
110
|
saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;
|
|
95
111
|
/** Unsaved changes, or not: filex asks before the person leaves them. */
|
|
@@ -339,6 +355,7 @@ function bridge(port: MessagePort, opts: ConnectOptions): FilexApp {
|
|
|
339
355
|
text: () => read('text', 'text'),
|
|
340
356
|
bytes: () => read('bytes', 'bytes'),
|
|
341
357
|
stream: () => read('stream', 'stream'),
|
|
358
|
+
read: (as: 'stream' | 'bytes' | 'text' = 'bytes') => request<ReadResult>('file.read', { index, as }),
|
|
342
359
|
save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),
|
|
343
360
|
};
|
|
344
361
|
},
|
|
@@ -346,6 +363,9 @@ function bridge(port: MessagePort, opts: ConnectOptions): FilexApp {
|
|
|
346
363
|
const p = await payload(data);
|
|
347
364
|
const params: Record<string, unknown> = { index: o.index ?? 0, data: p.data, mime: o.mime };
|
|
348
365
|
if (typeof o.through === 'number') params.through = o.through;
|
|
366
|
+
if (o.csv) params.csv = { ...o.csv };
|
|
367
|
+
if (o.beside) params.beside = o.beside;
|
|
368
|
+
if (o.conflict) params.conflict = o.conflict;
|
|
349
369
|
return request<SaveResult>('file.save', params, p.transfer);
|
|
350
370
|
},
|
|
351
371
|
async saveAs(name, data, mime) {
|
package/src/protocol.ts
CHANGED
|
@@ -268,8 +268,22 @@ export interface Session {
|
|
|
268
268
|
locale: string;
|
|
269
269
|
dir: 'ltr' | 'rtl';
|
|
270
270
|
theme: Theme;
|
|
271
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* The person's display name (their profile's, else their username); never
|
|
273
|
+
* their e-mail, never a token. '' where nobody is signed in (a public
|
|
274
|
+
* page). An editor writes it as the author of comments and tracked
|
|
275
|
+
* changes.
|
|
276
|
+
*/
|
|
272
277
|
user: { name: string };
|
|
278
|
+
/**
|
|
279
|
+
* filex 0.57: the language the administrator fixed for document editors
|
|
280
|
+
* (Admin → External services → ONLYOFFICE → Editor language,
|
|
281
|
+
* `FILEX_ONLYOFFICE_LANG`; a code such as `de`, `pt-PT`, `zh-TW`). An app
|
|
282
|
+
* that draws a document editor draws it in this language rather than the
|
|
283
|
+
* reader's `locale`; its own words stay in `locale`. Absent: each person's
|
|
284
|
+
* own language decides.
|
|
285
|
+
*/
|
|
286
|
+
editor_lang?: string;
|
|
273
287
|
files: FileInfo[];
|
|
274
288
|
/** The permissions this app was granted — so it can hide what it may not do. */
|
|
275
289
|
grants: string[];
|
|
@@ -285,6 +299,26 @@ export interface ReadParams {
|
|
|
285
299
|
as?: 'stream' | 'bytes' | 'text';
|
|
286
300
|
}
|
|
287
301
|
|
|
302
|
+
/**
|
|
303
|
+
* How a CSV file is written (filex 0.57): `ReadResult.text_dialect` of a
|
|
304
|
+
* `.csv`, read by the server from the file's own bytes.
|
|
305
|
+
*/
|
|
306
|
+
export interface TextDialect {
|
|
307
|
+
/** One character: `,`, `;`, `\t` or `|`. */
|
|
308
|
+
delimiter: string;
|
|
309
|
+
/** The file starts with a byte order mark. */
|
|
310
|
+
bom: boolean;
|
|
311
|
+
/** Its lines end in CRLF (else LF). */
|
|
312
|
+
crlf: boolean;
|
|
313
|
+
/**
|
|
314
|
+
* Its encoding as a browser's TextDecoder names it: `utf-8`, `utf-16le`,
|
|
315
|
+
* `utf-16be`, or a code page (`windows-1250`, `windows-1251`,
|
|
316
|
+
* `windows-1252`, `windows-1254`). `file.read` with `as: 'text'` decodes
|
|
317
|
+
* with it; `bytes` and `stream` hand the bytes as they are.
|
|
318
|
+
*/
|
|
319
|
+
encoding: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
288
322
|
export interface ReadResult {
|
|
289
323
|
name: string;
|
|
290
324
|
size: number;
|
|
@@ -292,6 +326,33 @@ export interface ReadResult {
|
|
|
292
326
|
stream?: ReadableStream<Uint8Array>;
|
|
293
327
|
bytes?: ArrayBuffer;
|
|
294
328
|
text?: string;
|
|
329
|
+
/**
|
|
330
|
+
* filex 0.57: how a `.csv` is written (its delimiter, byte order mark,
|
|
331
|
+
* line ends, encoding). Absent for any other file, for a file of an
|
|
332
|
+
* encrypted folder (the server holds its ciphertext only), and from filex
|
|
333
|
+
* 0.56 and older.
|
|
334
|
+
*/
|
|
335
|
+
text_dialect?: TextDialect;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* What an app says about the CSV it saves (filex 0.57; `SaveParams.csv`):
|
|
340
|
+
* how ITS bytes are written. filex reads the same from the bytes and refuses
|
|
341
|
+
* a save they contradict (`invalid`, message `csv_dialect`): nothing is
|
|
342
|
+
* written. Whatever the app wrote, the file is written in its own way - its
|
|
343
|
+
* delimiter, byte order mark, line ends and encoding - with every cell nobody
|
|
344
|
+
* changed as the file has it. A field left out is not checked; no `csv` at
|
|
345
|
+
* all is read from the bytes alone.
|
|
346
|
+
*/
|
|
347
|
+
export interface CsvDeclaration {
|
|
348
|
+
/** One character: `,`, `;`, `\t` or `|`. */
|
|
349
|
+
delimiter?: string;
|
|
350
|
+
/** As TextDialect.encoding names it (`utf-8` for a string the SDK sent). */
|
|
351
|
+
encoding?: string;
|
|
352
|
+
/** The bytes start with a byte order mark. */
|
|
353
|
+
bom?: boolean;
|
|
354
|
+
/** Their line ends. */
|
|
355
|
+
eol?: 'crlf' | 'lf';
|
|
295
356
|
}
|
|
296
357
|
|
|
297
358
|
/** `file.save` params: the new content of the opened file. */
|
|
@@ -306,11 +367,50 @@ export interface SaveParams {
|
|
|
306
367
|
* outside a session.
|
|
307
368
|
*/
|
|
308
369
|
through?: number;
|
|
370
|
+
/**
|
|
371
|
+
* filex 0.57: what the app says about the CSV it saves over a `.csv`
|
|
372
|
+
* (CsvDeclaration). Ignored for any other file and for a file of an
|
|
373
|
+
* encrypted folder; filex 0.56 and older ignore it.
|
|
374
|
+
*/
|
|
375
|
+
csv?: CsvDeclaration;
|
|
376
|
+
/**
|
|
377
|
+
* filex 0.57: the save is in this format (an extension: `docx`, `xlsx`,
|
|
378
|
+
* `pptx`, `docm`, `xlsm`, `pptm`, `odt`, `ods`, `odp`), which the opened
|
|
379
|
+
* file cannot hold (a `.doc`, `.xls`, `.ppt`...). It is written BESIDE the
|
|
380
|
+
* file, which is left as it is, under a name filex chooses - the file's own
|
|
381
|
+
* name with that extension, `rapor (2).docx` when that is taken - and only
|
|
382
|
+
* for a person who may create files in that folder (`permission_denied`
|
|
383
|
+
* otherwise). `SaveResult.name` says where; later saves of the same
|
|
384
|
+
* opening go to that file. The same extension as the file's own: an
|
|
385
|
+
* ordinary save over it. Refused (`unavailable`) for a file of an
|
|
386
|
+
* encrypted folder. filex 0.56 and older ignore it and write over the file.
|
|
387
|
+
*/
|
|
388
|
+
beside?: string;
|
|
389
|
+
/**
|
|
390
|
+
* filex 0.57: `'copy'` - when the file changed since the app opened it
|
|
391
|
+
* (somebody else saved it, a sync client wrote it), the save is written as
|
|
392
|
+
* a conflict copy beside it (`<name>.filex-conflict-<UTC time>.<ext>`)
|
|
393
|
+
* instead of being refused, and `SaveResult.conflict` names it; later saves
|
|
394
|
+
* of the same opening go to that copy. Without it such a save is refused
|
|
395
|
+
* (`failed`, message `changed`), as in 0.56. A file of an encrypted folder
|
|
396
|
+
* is never copied: refused as before.
|
|
397
|
+
*/
|
|
398
|
+
conflict?: 'copy';
|
|
309
399
|
}
|
|
310
400
|
|
|
311
401
|
export interface SaveResult {
|
|
312
402
|
saved: true;
|
|
313
403
|
size: number;
|
|
404
|
+
/**
|
|
405
|
+
* filex 0.57: the name of the file the save was written to when it is not
|
|
406
|
+
* the opened file - the one beside it (`beside`) or the conflict copy
|
|
407
|
+
* (`conflict`). Absent for a save over the opened file.
|
|
408
|
+
*/
|
|
409
|
+
name?: string;
|
|
410
|
+
/** filex 0.57: the save went beside the file, in the format `beside` named. */
|
|
411
|
+
beside?: true;
|
|
412
|
+
/** filex 0.57: the save went to this conflict copy (`conflict: 'copy'`). */
|
|
413
|
+
conflict?: string;
|
|
314
414
|
}
|
|
315
415
|
|
|
316
416
|
/**
|