@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 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 L(n){return!!n&&typeof n=="object"&&n.type===S&&typeof n.v=="number"}function x(n){return!!n&&typeof n=="object"&&n.type===B}function R(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||!x(g.data)||!g.ports[0]||d&&g.origin!==d)return;window.removeEventListener("message",b),clearTimeout(c);const s=g.ports[0],u=k(s,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 r=a.get(t.id);if(!r)return;a.delete(t.id),t.error?r.reject(new p(t.error)):r.resolve(t.result);return}if(typeof t.hid=="number"){b(t);return}if(typeof t.event=="string")for(const r of d.get(t.event)??[])try{r(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 r=t instanceof p?{code:t.code,message:t.message}:{code:"failed",message:String(t?.message??t)};n.postMessage({hid:e.hid,error:r})}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 s(e,t,r=[]){const i=++m;return new Promise((w,v)=>{a.set(i,{resolve:w,reject:v});try{n.postMessage({id:i,method:e,params:t},r)}catch(D){a.delete(i),v(D)}})}function u(e,t){s(e,t).catch(r=>console.warn("[filex-app-ui]",e,r))}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 r=(i,w)=>s("file.read",{index:e,as:i}).then(v=>v[w]);return{...t,text:()=>r("text","text"),bytes:()=>r("bytes","bytes"),stream:()=>r("stream","stream"),save:(i,w)=>y.save(i,{index:e,mime:w})}},async save(e,t={}){const r=await l(e),i={index:t.index??0,data:r.data,mime:t.mime};return typeof t.through=="number"&&(i.through=t.through),s("file.save",i,r.transfer)},async saveAs(e,t,r){const i=await l(t);return s("file.saveAs",{name:e,data:i.data,mime:r},i.transfer)},async download(e,t,r){const i=await l(t);return s("ui.download",{name:e,data:i.data,mime:r},i.transfer)},async print(e,t){const r=await l(t);return s("ui.print",{name:e,data:r.data,mime:"application/pdf"},r.transfer)},coedit:{join(e=0){return s("coedit.join",{index:e})},subscribe(e){return s("coedit.subscribe",{from:e})},append(e,t){return s("coedit.append",{kind:e,body:t})},async lease(e,t){return!!(await s("coedit.lease",{op:e,changesSeen:t}))?.granted},cursor(e){u("coedit.cursor",{cursor:e})},async putBlob(e,t){const r=await l(t);if(!(r.data instanceof ArrayBuffer))throw new p({code:"invalid",message:"a blob is bytes"});await s("coedit.blob.put",{name:e,data:r.data},r.transfer)},async getBlob(e){return(await s("coedit.blob.get",{name:e})).bytes},async leave(){await s("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 s("ui.confirm",typeof e=="string"?{text:e}:e)},close(){u("ui.close")},copy(e){return s("clipboard.write",{text:String(e)})},call(e,t){return s("engine.call",{method:e,params:t})},submit(e,t){return s("job.submit",{action:e,params:t})},state:{get(e){return s("state.get",{key:e})},set(e,t){return s("state.set",{key:e,value:t})}},license:{get(){return s("license.get")}},on(e,t){let r=d.get(e);return r||d.set(e,r=new Set),r.add(t),()=>r.delete(t)},onSave(e){return c=e,()=>{c===e&&(c=null)}},request:s};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=L,o.isKnownMethod=I,o.isPortMessage=x,o.isRequest=R,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),o}({});
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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"}
@@ -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 s = y.ports[0], c = T(s, n);
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 r = o.get(t.id);
114
- if (!r) return;
115
- o.delete(t.id), t.error ? r.reject(new m(t.error)) : r.resolve(t.result);
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 r of l.get(t.event) ?? [])
123
+ for (const s of l.get(t.event) ?? [])
124
124
  try {
125
- r(t.data);
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 r = t instanceof m ? { code: t.code, message: t.message } : { code: "failed", message: String(t?.message ?? t) };
142
- n.postMessage({ hid: e.hid, error: r });
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 s(e, t, r = []) {
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 }, r);
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
- s(e, t).catch((r) => console.warn("[filex-app-ui]", e, r));
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 r = (i, b) => s("file.read", { index: e, as: i }).then((w) => w[b]);
187
+ const s = (i, b) => r("file.read", { index: e, as: i }).then((w) => w[b]);
188
188
  return {
189
189
  ...t,
190
- text: () => r("text", "text"),
191
- bytes: () => r("bytes", "bytes"),
192
- stream: () => r("stream", "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 r = await f(e), i = { index: t.index ?? 0, data: r.data, mime: t.mime };
198
- return typeof t.through == "number" && (i.through = t.through), s("file.save", i, r.transfer);
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, r) {
201
+ async saveAs(e, t, s) {
201
202
  const i = await f(t);
202
- return s("file.saveAs", { name: e, data: i.data, mime: r }, i.transfer);
203
+ return r("file.saveAs", { name: e, data: i.data, mime: s }, i.transfer);
203
204
  },
204
- async download(e, t, r) {
205
+ async download(e, t, s) {
205
206
  const i = await f(t);
206
- return s("ui.download", { name: e, data: i.data, mime: r }, i.transfer);
207
+ return r("ui.download", { name: e, data: i.data, mime: s }, i.transfer);
207
208
  },
208
209
  async print(e, t) {
209
- const r = await f(t);
210
- return s("ui.print", { name: e, data: r.data, mime: "application/pdf" }, r.transfer);
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 s("coedit.join", { index: e });
215
+ return r("coedit.join", { index: e });
215
216
  },
216
217
  subscribe(e) {
217
- return s("coedit.subscribe", { from: e });
218
+ return r("coedit.subscribe", { from: e });
218
219
  },
219
220
  append(e, t) {
220
- return s("coedit.append", { kind: e, body: t });
221
+ return r("coedit.append", { kind: e, body: t });
221
222
  },
222
223
  async lease(e, t) {
223
- return !!(await s("coedit.lease", { op: e, changesSeen: t }))?.granted;
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 r = await f(t);
230
- if (!(r.data instanceof ArrayBuffer)) throw new m({ code: "invalid", message: "a blob is bytes" });
231
- await s("coedit.blob.put", { name: e, data: r.data }, r.transfer);
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 s("coedit.blob.get", { name: e })).bytes;
235
+ return (await r("coedit.blob.get", { name: e })).bytes;
235
236
  },
236
237
  async leave() {
237
- await s("coedit.leave");
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 s("ui.confirm", typeof e == "string" ? { text: e } : e);
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 s("clipboard.write", { text: String(e) });
257
+ return r("clipboard.write", { text: String(e) });
257
258
  },
258
259
  call(e, t) {
259
- return s("engine.call", { method: e, params: t });
260
+ return r("engine.call", { method: e, params: t });
260
261
  },
261
262
  submit(e, t) {
262
- return s("job.submit", { action: e, params: t });
263
+ return r("job.submit", { action: e, params: t });
263
264
  },
264
265
  state: {
265
266
  get(e) {
266
- return s("state.get", { key: e });
267
+ return r("state.get", { key: e });
267
268
  },
268
269
  set(e, t) {
269
- return s("state.set", { key: e, value: t });
270
+ return r("state.set", { key: e, value: t });
270
271
  }
271
272
  },
272
273
  license: {
273
274
  get() {
274
- return s("license.get");
275
+ return r("license.get");
275
276
  }
276
277
  },
277
278
  on(e, t) {
278
- let r = l.get(e);
279
- return r || l.set(e, r = /* @__PURE__ */ new Set()), r.add(t), () => r.delete(t);
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: s
287
+ request: r
287
288
  };
288
289
  return u.saveShortcut !== !1 && typeof document < "u" && document.addEventListener(
289
290
  "keydown",
@@ -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
- /** The person's display name; never their e-mail, never a token. */
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.56.3",
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(data: SaveData, opts?: { index?: number; mime?: string; through?: number }): Promise<SaveResult>;
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
- /** The person's display name; never their e-mail, never a token. */
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
  /**