@brftech/filex-app-ui 0.55.0 → 0.56.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 +3 -3
- package/dist/filex-app-ui.iife.js +1 -1
- package/dist/filex-app-ui.iife.js.map +1 -1
- package/dist/filex-app-ui.js +20 -13
- package/dist/filex-app-ui.js.map +1 -1
- package/dist/index.d.ts +103 -24
- package/package.json +1 -1
- package/src/index.ts +22 -9
- package/src/protocol.ts +85 -18
package/README.md
CHANGED
|
@@ -30,16 +30,16 @@ with a `<script src>`: it defines `window.FilexAppUI.connect`.
|
|
|
30
30
|
|
|
31
31
|
| Call | What it does |
|
|
32
32
|
|---|---|
|
|
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 - information only: filex
|
|
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
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 |
|
|
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 |
|
|
38
38
|
| `fx.print(name, pdf)` | print a PDF (filex 0.55; `"ui": { "print": true }` in the manifest): a sandboxed frame may not open the print dialog, so filex prints it from its own page - filex asks every time and prints on the person's Allow, 64 MiB at most (`LIMITS.maxPrintBytes`); filex 0.54 and older answer `unknown_method`, a host whose print page may not be shown `unavailable` |
|
|
39
|
-
| `fx.coedit.join()` / `.subscribe(from)` / `.append(kind, body)` / `.lease(op, seen)` / `.cursor(c)` / `.putBlob` / `.getBlob` / `.leave()` | editing together: filex seals the session's log and orders it through its relay, the app never holds a key.
|
|
39
|
+
| `fx.coedit.join()` / `.subscribe(from)` / `.append(kind, body)` / `.lease(op, seen)` / `.cursor(c)` / `.putBlob` / `.getBlob` / `.leave()` | editing together (filex 0.56, the manifest's `"co_edit": {"open": true}`, granted as `files:co-edit`): filex seals the session's log and orders it through its relay, the app never holds a key. `join` answers `created` when this member started the session - it puts the base document (`putBlob('base', …)`) before anybody else may join; `fx.save(data, {through})` tells the session what a save holds. filex 0.55 answers each `unavailable` - edit alone then |
|
|
40
40
|
| `fx.call(method, params)` | ask the app's own module (`ui_call`) |
|
|
41
41
|
| `fx.submit(action, params)` | queue one of the app's actions on the opened files - like `copy`, from a click, or filex asks |
|
|
42
|
-
| `fx.state.get(key)` / `fx.state.set(key, value)` | a small store for this app and this person |
|
|
42
|
+
| `fx.state.get(key)` / `fx.state.set(key, value)` | a small store for this app and this person (with a file of an encrypted folder open in the clear: a separate store, sealed in the browser) |
|
|
43
43
|
| `fx.license.get()` | the app's license, for a paid app installed from a store (filex 0.52.0): `{status: 'free'}`, or `{status: 'valid', valid_until, updates_until}` - never the key, never the licensee |
|
|
44
44
|
| `fx.on('theme' \| 'locale' \| 'file.changed' \| 'close.request' \| 'app.updated', fn)` | listen to filex (editing together adds `coedit.entry`, `coedit.cursor`, `coedit.dropped`) |
|
|
45
45
|
|
|
@@ -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"],
|
|
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}({});
|
|
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 * 0.55.0, editing together (`coedit.*`, task #189): the session's log the\n * host seals, orders through filex's relay and opens again. Defined so an\n * app can be written against them; filex 0.55 offers no relay yet, so each\n * answers `unavailable` - edit alone then (see `CoEditHello`).\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 (the app fell\n * behind, or the connection dropped). `data: {from}` - subscribe again\n * from the last entry the app holds.\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 * ⚠ filex 0.55 decrypts nothing for an app: the server holds such a file\n * only as ciphertext and has no key, so `file.read` is refused\n * (`failed` with `encrypted`), a save is refused, and `readOnly` is true.\n * The explorer offers no app an encrypted file in the first place; the\n * field is information, not a promise of plaintext.\n */\n encrypted?: 'folder' | 'vault' | 'file';\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.0: defined, not offered) ────── */\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, a vault, a file\n * that cannot be shared): the app edits alone.\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /** The last entry's seq (0: an empty log). */\n head: number;\n /** The last `changes` entry's seq. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\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. */\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} 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 /** Save new content over an opened file. */\n save(data: SaveData, opts?: { index?: number; mime?: string }): 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.55: defined, not offered - every call answers\n * `unavailable`, and the app edits alone). filex seals what the app\n * appends, orders it through its relay and hands every member the same\n * entries in the same order (`coedit.entry` events after `subscribe`);\n * the app never holds a key.\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 /** This person's small store for this app (JSON values, 8 KiB each). */\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 return request<SaveResult>('file.save', { index: o.index ?? 0, data: p.data, mime: o.mime }, 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 /** 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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"}
|
package/dist/filex-app-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const j = 1, S = "filex:hello",
|
|
1
|
+
const j = 1, S = "filex:hello", M = "filex:port", E = [
|
|
2
2
|
"session.get",
|
|
3
3
|
"file.read",
|
|
4
4
|
"file.save",
|
|
@@ -41,13 +41,20 @@ const j = 1, S = "filex:hello", B = "filex:port", E = [
|
|
|
41
41
|
* Blob) until the print dialog has read it, and a document worth printing
|
|
42
42
|
* is far smaller.
|
|
43
43
|
*/
|
|
44
|
-
maxPrintBytes: 67108864
|
|
44
|
+
maxPrintBytes: 67108864,
|
|
45
|
+
/**
|
|
46
|
+
* The largest file of an encrypted folder filex hands an app in the clear
|
|
47
|
+
* or takes back from it (filex 0.56, `FileInfo.plaintext`): it is
|
|
48
|
+
* decrypted and encrypted whole in the page, as filex's own explorer does
|
|
49
|
+
* for a file of this size.
|
|
50
|
+
*/
|
|
51
|
+
maxEncryptedBytes: 209715200
|
|
45
52
|
};
|
|
46
53
|
function k(n) {
|
|
47
54
|
return !!n && typeof n == "object" && n.type === S && typeof n.v == "number";
|
|
48
55
|
}
|
|
49
56
|
function L(n) {
|
|
50
|
-
return !!n && typeof n == "object" && n.type ===
|
|
57
|
+
return !!n && typeof n == "object" && n.type === M;
|
|
51
58
|
}
|
|
52
59
|
function A(n) {
|
|
53
60
|
return !!n && typeof n == "object" && typeof n.id == "number" && typeof n.method == "string";
|
|
@@ -60,9 +67,9 @@ class m extends Error {
|
|
|
60
67
|
super(u.message || u.code), this.name = "FilexError", this.code = u.code;
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
|
-
let
|
|
70
|
+
let h = null;
|
|
64
71
|
function P(n = {}) {
|
|
65
|
-
return
|
|
72
|
+
return h || (h = O(n)), h;
|
|
66
73
|
}
|
|
67
74
|
function O(n) {
|
|
68
75
|
return new Promise((u, p) => {
|
|
@@ -78,9 +85,9 @@ function O(n) {
|
|
|
78
85
|
window.removeEventListener("message", g), clearTimeout(a);
|
|
79
86
|
const s = y.ports[0], c = T(s, n);
|
|
80
87
|
c.request("session.get").then((f) => {
|
|
81
|
-
c.session = f, n.applyTheme !== !1 && (
|
|
88
|
+
c.session = f, n.applyTheme !== !1 && (v(f.theme, f.locale, f.dir), c.on("theme", (d) => v(d)), c.on("locale", (d) => {
|
|
82
89
|
const e = d;
|
|
83
|
-
|
|
90
|
+
v(void 0, e.locale, e.dir);
|
|
84
91
|
})), u(c);
|
|
85
92
|
}).catch(p);
|
|
86
93
|
}
|
|
@@ -149,8 +156,8 @@ function T(n, u) {
|
|
|
149
156
|
o.set(i, { resolve: b, reject: w });
|
|
150
157
|
try {
|
|
151
158
|
n.postMessage({ id: i, method: e, params: t }, r);
|
|
152
|
-
} catch (
|
|
153
|
-
o.delete(i), w(
|
|
159
|
+
} catch (B) {
|
|
160
|
+
o.delete(i), w(B);
|
|
154
161
|
}
|
|
155
162
|
});
|
|
156
163
|
}
|
|
@@ -187,8 +194,8 @@ function T(n, u) {
|
|
|
187
194
|
};
|
|
188
195
|
},
|
|
189
196
|
async save(e, t = {}) {
|
|
190
|
-
const r = await f(e);
|
|
191
|
-
return
|
|
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);
|
|
192
199
|
},
|
|
193
200
|
async saveAs(e, t, r) {
|
|
194
201
|
const i = await f(t);
|
|
@@ -286,7 +293,7 @@ function T(n, u) {
|
|
|
286
293
|
!0
|
|
287
294
|
), d;
|
|
288
295
|
}
|
|
289
|
-
function
|
|
296
|
+
function v(n, u, p) {
|
|
290
297
|
if (typeof document > "u") return;
|
|
291
298
|
const o = document.documentElement;
|
|
292
299
|
if (n) {
|
|
@@ -302,7 +309,7 @@ export {
|
|
|
302
309
|
S as HELLO,
|
|
303
310
|
x as LIMITS,
|
|
304
311
|
E as METHODS,
|
|
305
|
-
|
|
312
|
+
M as PORT,
|
|
306
313
|
P as connect,
|
|
307
314
|
k as isHello,
|
|
308
315
|
I as isKnownMethod,
|
package/dist/filex-app-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get'\n /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */\n | 'ui.print'\n /**\n * 0.55.0, editing together (`coedit.*`, task #189): the session's log the\n * host seals, orders through filex's relay and opens again. Defined so an\n * app can be written against them; filex 0.55 offers no relay yet, so each\n * answers `unavailable` - edit alone then (see `CoEditHello`).\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 (the app fell\n * behind, or the connection dropped). `data: {from}` - subscribe again\n * from the last entry the app holds.\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 * ⚠ filex 0.55 decrypts nothing for an app: the server holds such a file\n * only as ciphertext and has no key, so `file.read` is refused\n * (`failed` with `encrypted`), a save is refused, and `readOnly` is true.\n * The explorer offers no app an encrypted file in the first place; the\n * field is information, not a promise of plaintext.\n */\n encrypted?: 'folder' | 'vault' | 'file';\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.0: defined, not offered) ────── */\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, a vault, a file\n * that cannot be shared): the app edits alone.\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /** The last entry's seq (0: an empty log). */\n head: number;\n /** The last `changes` entry's seq. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\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. */\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} 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 /** Save new content over an opened file. */\n save(data: SaveData, opts?: { index?: number; mime?: string }): 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.55: defined, not offered - every call answers\n * `unavailable`, and the app edits alone). filex seals what the app\n * appends, orders it through its relay and hands every member the same\n * entries in the same order (`coedit.entry` events after `subscribe`);\n * the app never holds a key.\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 /** This person's small store for this app (JSON values, 8 KiB each). */\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 return request<SaveResult>('file.save', { index: o.index ?? 0, data: p.data, mime: o.mime }, 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":"AA6BO,MAAMA,IAAiB,GAGjBC,IAAQ,eAERC,IAAO,cAsCPC,IAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAyZaC,IAAS;AAAA;AAAA,EAEpB,cAAc;AAAA;AAAA,EAEd,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlB,eAAe;AACjB;AAIO,SAASC,EAAQC,GAA+B;AACrD,SAAO,CAAC,CAACA,KAAK,OAAOA,KAAM,YAAaA,EAAmB,SAASL,KAAS,OAAQK,EAAmB,KAAM;AAChH;AAEO,SAASC,EAAcD,GAA8B;AAC1D,SAAO,CAAC,CAACA,KAAK,OAAOA,KAAM,YAAaA,EAAkB,SAASJ;AACrE;AAEO,SAASM,EAAUF,GAAiC;AACzD,SACE,CAAC,CAACA,KACF,OAAOA,KAAM,YACb,OAAQA,EAAqB,MAAO,YACpC,OAAQA,EAAqB,UAAW;AAE5C;AAEO,SAASG,EAAcC,GAAwB;AACpD,SAAQP,EAA8B,SAASO,CAAC;AAClD;ACnfO,MAAMC,UAAmB,MAAM;AAAA,EAEpC,YAAYC,GAAkB;AAC5B,UAAMA,EAAI,WAAWA,EAAI,IAAI,GAC7B,KAAK,OAAO,cACZ,KAAK,OAAOA,EAAI;AAAA,EAClB;AACF;AAwGA,IAAIC,IAAuC;AAOpC,SAASC,EAAQC,IAAuB,IAAuB;AACpE,SAAKF,MAAYA,IAAaG,EAAKD,CAAI,IAChCF;AACT;AAEA,SAASG,EAAKD,GAAyC;AACrD,SAAO,IAAI,QAAkB,CAACE,GAASC,MAAW;AAChD,QAAI,OAAO,SAAW,OAAe,OAAO,WAAW,QAAQ;AAC7D,MAAAA,EAAO,IAAIP,EAAW,EAAE,MAAM,eAAe,SAAS,2BAAA,CAA4B,CAAC;AACnF;AAAA,IACF;AACA,UAAMQ,IAAS,OAAO,QAKhBC,IAAWC,EAAA,GACXC,IAAQ,WAAW,MAAM;AAC7B,aAAO,oBAAoB,WAAWC,CAAS,GAC/CL,EAAO,IAAIP,EAAW,EAAE,MAAM,eAAe,SAAS,uBAAA,CAAwB,CAAC;AAAA,IACjF,GAAGI,EAAK,aAAaX,EAAO,gBAAgB;AAE5C,aAASmB,EAAUC,GAAkB;AAEnC,UADIA,EAAG,WAAWL,KAAU,CAACZ,EAAciB,EAAG,IAAI,KAAK,CAACA,EAAG,MAAM,CAAC,KAC9DJ,KAAYI,EAAG,WAAWJ,EAAU;AACxC,aAAO,oBAAoB,WAAWG,CAAS,GAC/C,aAAaD,CAAK;AAClB,YAAMG,IAAOD,EAAG,MAAM,CAAC,GACjBE,IAAMC,EAAOF,GAAMV,CAAI;AAC7B,MAAAW,EACG,QAAiB,aAAa,EAC9B,KAAK,CAACE,MAAY;AAChB,QAAAF,EAA6B,UAAUE,GACpCb,EAAK,eAAe,OACtBc,EAAUD,EAAQ,OAAOA,EAAQ,QAAQA,EAAQ,GAAG,GACpDF,EAAI,GAAG,SAAS,CAACI,MAAMD,EAAUC,CAAU,CAAC,GAC5CJ,EAAI,GAAG,UAAU,CAACK,MAAM;AACtB,gBAAMC,IAAID;AACV,UAAAF,EAAU,QAAWG,EAAE,QAAQA,EAAE,GAAG;AAAA,QACtC,CAAC,IAEHf,EAAQS,CAAG;AAAA,MACb,CAAC,EACA,MAAMR,CAAM;AAAA,IACjB;AACA,WAAO,iBAAiB,WAAWK,CAAS,GAC5CJ,EAAO,YAAY,EAAE,MAAMlB,GAAO,GAAG,EAAA,GAAkB,GAAG;AAAA,EAC5D,CAAC;AACH;AAEA,SAASoB,IAAyB;AAChC,MAAI;AACF,UAAMY,IAAQ,OAAO,SAA4D;AACjF,QAAIA,KAAQA,EAAK,SAAS,KAAKA,EAAK,CAAC,MAAM,OAAQ,QAAOA,EAAK,CAAC;AAAA,EAClE,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,SAASN,EAAOF,GAAmBV,GAAgC;AACjE,MAAImB,IAAM;AACV,QAAMC,wBAAc,IAAA,GACdC,wBAAgB,IAAA;AACtB,MAAIC,IAAyE;AAE7E,EAAAZ,EAAK,YAAY,CAACD,MAAqB;AACrC,UAAMd,IAAIc,EAAG;AACb,QAAI,GAACd,KAAK,OAAOA,KAAM,WACvB;AAAA,UAAI,OAAOA,EAAE,MAAO,UAAU;AAC5B,cAAM4B,IAAIH,EAAQ,IAAIzB,EAAE,EAAE;AAC1B,YAAI,CAAC4B,EAAG;AACR,QAAAH,EAAQ,OAAOzB,EAAE,EAAE,GACfA,EAAE,QAAO4B,EAAE,OAAO,IAAI3B,EAAWD,EAAE,KAAoB,CAAC,IACvD4B,EAAE,QAAQ5B,EAAE,MAAM;AACvB;AAAA,MACF;AACA,UAAI,OAAOA,EAAE,OAAQ,UAAU;AAC7B,QAAK6B,EAAW7B,CAAkC;AAClD;AAAA,MACF;AACA,UAAI,OAAOA,EAAE,SAAU;AACrB,mBAAW8B,KAAKJ,EAAU,IAAI1B,EAAE,KAAkB,KAAK;AACrD,cAAI;AACF,YAAA8B,EAAE9B,EAAE,IAAI;AAAA,UACV,SAAS+B,GAAG;AACV,oBAAQ,MAAM,kCAAkCA,CAAC;AAAA,UACnD;AAAA;AAAA,EAGN;AAEA,iBAAeF,EAAW7B,GAAuB;AAC/C,QAAIA,EAAE,YAAY,QAAQ;AACxB,UAAI,CAAC2B,GAAa;AAChB,QAAAZ,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,SAAS,+BAAA,EAA+B,CAAG;AACxG;AAAA,MACF;AACA,UAAI;AACF,cAAMgC,IAAM,MAAMC,EAAA;AAClB,QAAAlB,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,QAAQgC,KAAO,EAAE,OAAO,GAAA,EAAK,CAAG;AAAA,MACjE,SAASD,GAAG;AACV,cAAM7B,IAAM6B,aAAa9B,IAAa,EAAE,MAAM8B,EAAE,MAAM,SAASA,EAAE,YAAY,EAAE,MAAM,UAAU,SAAS,OAAQA,GAAa,WAAWA,CAAC,EAAA;AACzI,QAAAhB,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,OAAOE,GAAK;AAAA,MAC7C;AACA;AAAA,IACF;AACA,IAAAa,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAA,GAAoB;AAAA,EACpE;AAEA,iBAAeiC,IAAsC;AACnD,QAAI,CAACN,EAAa,QAAO;AACzB,UAAMO,IAAO,MAAMP,EAAA;AACnB,WAA0BO,KAAS,OAAa,OACzClB,EAAI,KAAKkB,CAAgB;AAAA,EAClC;AAEA,WAASC,EAAWC,GAAgBC,GAAkBC,IAA2B,CAAA,GAAgB;AAC/F,UAAMC,IAAK,EAAEf;AACb,WAAO,IAAI,QAAW,CAACjB,GAASC,MAAW;AACzC,MAAAiB,EAAQ,IAAIc,GAAI,EAAE,SAAAhC,GAA0C,QAAAC,GAAQ;AACpE,UAAI;AACF,QAAAO,EAAK,YAAY,EAAE,IAAAwB,GAAI,QAAAH,GAAQ,QAAAC,EAAA,GAAUC,CAAQ;AAAA,MACnD,SAASP,GAAG;AACV,QAAAN,EAAQ,OAAOc,CAAE,GACjB/B,EAAOuB,CAAC;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAEA,WAASS,EAAOJ,GAAgBC,GAAkB;AAChD,IAAAF,EAAQC,GAAQC,CAAM,EAAE,MAAM,CAACN,MAAM,QAAQ,KAAK,kBAAkBK,GAAQL,CAAC,CAAC;AAAA,EAChF;AAEA,iBAAeU,EAAQP,GAAgH;AACrI,QAAI,OAAOA,KAAS,SAAU,QAAO,EAAE,MAAAA,GAAM,UAAU,GAAC;AACxD,QAAIA,aAAgB,YAAa,QAAO,EAAE,MAAAA,GAAM,UAAU,CAACA,CAAI,EAAA;AAC/D,QAAI,YAAY,OAAOA,CAAI,GAAG;AAC5B,YAAMQ,IAAOR,EAAK,OAAO,MAAMA,EAAK,YAAYA,EAAK,aAAaA,EAAK,UAAU;AACjF,aAAO,EAAE,MAAMQ,GAAM,UAAU,CAACA,CAAI,EAAA;AAAA,IACtC;AACA,QAAI,OAAO,OAAS,OAAeR,aAAgB,MAAM;AACvD,YAAMS,IAAM,MAAMT,EAAK,YAAA;AACvB,aAAO,EAAE,MAAMS,GAAK,UAAU,CAACA,CAAG,EAAA;AAAA,IACpC;AACA,QAAI,OAAO,iBAAmB,OAAeT,aAAgB;AAC3D,aAAO,EAAE,MAAAA,GAAM,UAAU,CAACA,CAA+B,EAAA;AAE3D,UAAM,IAAIjC,EAAW,EAAE,MAAM,WAAW,SAAS,0DAA0D;AAAA,EAC7G;AAEA,QAAMe,IAAgB;AAAA,IACpB,SAAS;AAAA,IACT,MAAM,KAAK4B,IAAQ,GAAG;AACpB,YAAMC,IAAO7B,EAAI,SAAS,QAAQ4B,CAAK;AACvC,UAAI,CAACC,EAAM,OAAM,IAAI5C,EAAW,EAAE,MAAM,aAAa,SAAS,cAAc2C,CAAK,GAAA,CAAI;AACrF,YAAME,IAAO,CAA6BC,GAAiCC,MACzEb,EAAoB,aAAa,EAAE,OAAAS,GAAO,IAAAG,EAAA,CAAI,EAAE,KAAK,CAACE,MAAMA,EAAED,CAAG,CAA+B;AAClG,aAAO;AAAA,QACL,GAAGH;AAAA,QACH,MAAM,MAAMC,EAAK,QAAQ,MAAM;AAAA,QAC/B,OAAO,MAAMA,EAAK,SAAS,OAAO;AAAA,QAClC,QAAQ,MAAMA,EAAK,UAAU,QAAQ;AAAA,QACrC,MAAM,CAACZ,GAAgBgB,MAAkBlC,EAAI,KAAKkB,GAAM,EAAE,OAAAU,GAAO,MAAAM,EAAA,CAAM;AAAA,MAAA;AAAA,IAE3E;AAAA,IACA,MAAM,KAAKhB,GAAMiB,IAAI,IAAI;AACvB,YAAMvB,IAAI,MAAMa,EAAQP,CAAI;AAC5B,aAAOC,EAAoB,aAAa,EAAE,OAAOgB,EAAE,SAAS,GAAG,MAAMvB,EAAE,MAAM,MAAMuB,EAAE,KAAA,GAAQvB,EAAE,QAAQ;AAAA,IACzG;AAAA,IACA,MAAM,OAAOwB,GAAMlB,GAAMgB,GAAM;AAC7B,YAAMtB,IAAI,MAAMa,EAAQP,CAAI;AAC5B,aAAOC,EAAsB,eAAe,EAAE,MAAAiB,GAAM,MAAMxB,EAAE,MAAM,MAAAsB,EAAA,GAAQtB,EAAE,QAAQ;AAAA,IACtF;AAAA,IACA,MAAM,SAASwB,GAAMlB,GAAMgB,GAAM;AAC/B,YAAMtB,IAAI,MAAMa,EAAQP,CAAI;AAC5B,aAAOC,EAAwB,eAAe,EAAE,MAAAiB,GAAM,MAAMxB,EAAE,MAAM,MAAAsB,EAAA,GAAQtB,EAAE,QAAQ;AAAA,IACxF;AAAA,IACA,MAAM,MAAMwB,GAAMC,GAAK;AACrB,YAAMzB,IAAI,MAAMa,EAAQY,CAAG;AAC3B,aAAOlB,EAAqB,YAAY,EAAE,MAAAiB,GAAM,MAAMxB,EAAE,MAAM,MAAM,qBAAqBA,EAAE,QAAQ;AAAA,IACrG;AAAA,IACA,QAAQ;AAAA,MACN,KAAKgB,IAAQ,GAAG;AACd,eAAOT,EAAqB,eAAe,EAAE,OAAAS,GAAO;AAAA,MACtD;AAAA,MACA,UAAUU,GAAc;AACtB,eAAOnB,EAAc,oBAAoB,EAAE,MAAAmB,GAAM;AAAA,MACnD;AAAA,MACA,OAAOC,GAAkBC,GAAe;AACtC,eAAOrB,EAA4B,iBAAiB,EAAE,MAAAoB,GAAM,MAAAC,GAAM;AAAA,MACpE;AAAA,MACA,MAAM,MAAMC,GAA2BC,GAAqB;AAE1D,eAAO,CAAC,EADE,MAAMvB,EAA2B,gBAAgB,EAAE,IAAAsB,GAAI,aAAAC,GAAa,IAClE;AAAA,MACd;AAAA,MACA,OAAOC,GAAiB;AACtB,QAAAnB,EAAO,iBAAiB,EAAE,QAAAmB,GAAQ;AAAA,MACpC;AAAA,MACA,MAAM,QAAQP,GAAclB,GAA4C;AACtE,cAAMN,IAAI,MAAMa,EAAQP,CAAI;AAC5B,YAAI,EAAEN,EAAE,gBAAgB,aAAc,OAAM,IAAI3B,EAAW,EAAE,MAAM,WAAW,SAAS,kBAAA,CAAmB;AAC1G,cAAMkC,EAAc,mBAAmB,EAAE,MAAAiB,GAAM,MAAMxB,EAAE,KAAA,GAAQA,EAAE,QAAQ;AAAA,MAC3E;AAAA,MACA,MAAM,QAAQwB,GAAc;AAE1B,gBADU,MAAMjB,EAA6B,mBAAmB,EAAE,MAAAiB,GAAM,GAC/D;AAAA,MACX;AAAA,MACA,MAAM,QAAQ;AACZ,cAAMjB,EAAc,cAAc;AAAA,MACpC;AAAA,IAAA;AAAA,IAEF,MAAMyB,GAAI;AACR,MAAApB,EAAO,YAAY,EAAE,OAAO,CAAC,CAACoB,GAAI;AAAA,IACpC;AAAA,IACA,MAAMC,GAAM;AACV,MAAArB,EAAO,YAAY,EAAE,MAAM,OAAOqB,CAAI,EAAE,MAAM,GAAGnE,EAAO,YAAY,EAAA,CAAG;AAAA,IACzE;AAAA,IACA,MAAMmE,GAAMC,GAAM;AAChB,MAAAtB,EAAO,YAAY,EAAE,MAAM,OAAOqB,CAAI,EAAE,MAAM,GAAGnE,EAAO,YAAY,GAAG,MAAAoE,EAAA,CAAM;AAAA,IAC/E;AAAA,IACA,QAAQX,GAAG;AAET,aAAOhB,EAAiB,cADT,OAAOgB,KAAM,WAAW,EAAE,MAAMA,MAAMA,CACT;AAAA,IAC9C;AAAA,IACA,QAAQ;AACN,MAAAX,EAAO,UAAU;AAAA,IACnB;AAAA,IACA,KAAKqB,GAAM;AACT,aAAO1B,EAAc,mBAAmB,EAAE,MAAM,OAAO0B,CAAI,GAAG;AAAA,IAChE;AAAA,IACA,KAAQzB,GAAgBC,GAAkB;AACxC,aAAOF,EAAW,eAAe,EAAE,QAAAC,GAAQ,QAAAC,GAAQ;AAAA,IACrD;AAAA,IACA,OAAO0B,GAAQ1B,GAAQ;AACrB,aAAOF,EAAyB,cAAc,EAAE,QAAA4B,GAAQ,QAAA1B,GAAQ;AAAA,IAClE;AAAA,IACA,OAAO;AAAA,MACL,IAAOW,GAAa;AAClB,eAAOb,EAAuB,aAAa,EAAE,KAAAa,GAAK;AAAA,MACpD;AAAA,MACA,IAAIA,GAAagB,GAAgB;AAC/B,eAAO7B,EAAc,aAAa,EAAE,KAAAa,GAAK,OAAAgB,GAAO;AAAA,MAClD;AAAA,IAAA;AAAA,IAEF,SAAS;AAAA,MACP,MAAM;AACJ,eAAO7B,EAAqB,aAAa;AAAA,MAC3C;AAAA,IAAA;AAAA,IAEF,GAAG8B,GAAOC,GAAS;AACjB,UAAIC,IAAMzC,EAAU,IAAIuC,CAAK;AAC7B,aAAKE,KAAKzC,EAAU,IAAIuC,GAAQE,IAAM,oBAAI,KAAM,GAChDA,EAAI,IAAID,CAAO,GACR,MAAMC,EAAK,OAAOD,CAAO;AAAA,IAClC;AAAA,IACA,OAAOA,GAAS;AACd,aAAAvC,IAAcuC,GACP,MAAM;AACX,QAAIvC,MAAgBuC,MAASvC,IAAc;AAAA,MAC7C;AAAA,IACF;AAAA,IACA,SAAAQ;AAAA,EAAA;AAGF,SAAI9B,EAAK,iBAAiB,MAAS,OAAO,WAAa,OAIrD,SAAS;AAAA,IACP;AAAA,IACA,CAAC,MAAM;AACL,OAAK,EAAE,WAAW,EAAE,YAAY,CAAC,EAAE,UAAU,CAAC,EAAE,aAAa,EAAE,QAAQ,OAAO,EAAE,QAAQ,QAAQsB,MAC9F,EAAE,eAAA,GACFM,IAAU,MAAM,CAAC/B,MAAQc,EAAI,MAAM,OAAQd,GAAe,WAAWA,CAAG,GAAG,OAAO,CAAC;AAAA,IAEvF;AAAA,IACA;AAAA,EAAA,GAGGc;AACT;AAGA,SAASG,EAAUiD,GAAeC,GAAiBC,GAAqB;AACtE,MAAI,OAAO,WAAa,IAAa;AACrC,QAAMC,IAAO,SAAS;AACtB,MAAIH,GAAO;AACT,IAAAG,EAAK,QAAQ,QAAQH,EAAM,MAC3BG,EAAK,MAAM,cAAcH,EAAM;AAC/B,eAAW,CAACI,GAAGlD,CAAC,KAAK,OAAO,QAAQ8C,EAAM,UAAU,CAAA,CAAE;AACpD,MAAI,oBAAoB,KAAKI,CAAC,KAAK,OAAOlD,KAAM,YAAUiD,EAAK,MAAM,YAAYC,GAAGlD,CAAC;AAAA,EAEzF;AACA,EAAI+C,QAAa,OAAOA,IACpBC,QAAU,MAAMA;AACtB;"}
|
|
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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -39,11 +39,22 @@ export declare interface CoEditCursor {
|
|
|
39
39
|
cursor: unknown;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** `coedit.cursor` params: passed to the other members, not kept.
|
|
42
|
+
/** `coedit.cursor` params: passed to the other members, not kept. The host
|
|
43
|
+
* sends a few a second at most (the last one wins). */
|
|
43
44
|
export declare interface CoEditCursorParams {
|
|
44
45
|
cursor: unknown;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** `coedit.dropped` event data (filex 0.56). */
|
|
49
|
+
export declare interface CoEditDropped {
|
|
50
|
+
/** The last entry the host handed over. */
|
|
51
|
+
from: number;
|
|
52
|
+
/** `gone`: the membership ended (the session closed, or the page was away
|
|
53
|
+
* too long) - join again. `broken`: an entry did not check out (missing,
|
|
54
|
+
* moved, altered, replayed) - stop editing together. */
|
|
55
|
+
reason: 'gone' | 'broken';
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
/** One entry of the session's log, opened and checked by filex. */
|
|
48
59
|
export declare type CoEditEntry = (CoEditEntryBase & {
|
|
49
60
|
kind: CoEditKind;
|
|
@@ -69,18 +80,36 @@ declare interface CoEditEntryBase {
|
|
|
69
80
|
* What `coedit.join` answers: who the app is in the session and how far
|
|
70
81
|
* the log has gone. The entries come as `coedit.entry` events once the app
|
|
71
82
|
* subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`
|
|
72
|
-
* when filex offers no editing together here (filex 0.55,
|
|
73
|
-
*
|
|
83
|
+
* when filex offers no editing together here (filex 0.55, editing together
|
|
84
|
+
* turned off, an encrypted file, a vault): the app edits alone - the message
|
|
85
|
+
* says why (`not_ready` after waiting for a session's creator, `encrypted`,
|
|
86
|
+
* `nothing_to_watch` for a person who may only read the file while nobody
|
|
87
|
+
* edits it, or the server's code: `coedit_off`, `coedit_build`,
|
|
88
|
+
* `coedit_file_changed`).
|
|
74
89
|
*/
|
|
75
90
|
export declare interface CoEditHello {
|
|
76
91
|
session: string;
|
|
77
92
|
me: CoEditMember;
|
|
78
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* The last entry's seq when filex handed the session over (0: an empty
|
|
95
|
+
* log). filex 0.56: the changes this browser kept from an earlier opening
|
|
96
|
+
* of the file (a lost connection, a closed tab), which filex sends before
|
|
97
|
+
* it answers, are in the log up to here - an editor that starts once it
|
|
98
|
+
* has read this far opens with them.
|
|
99
|
+
*/
|
|
79
100
|
head: number;
|
|
80
|
-
/** The last `changes` entry's seq
|
|
101
|
+
/** The last `changes` entry's seq, as far as `head`. */
|
|
81
102
|
changesHead: number;
|
|
82
103
|
/** The last entry the file on the storage holds. */
|
|
83
104
|
savedThrough: number;
|
|
105
|
+
/**
|
|
106
|
+
* filex 0.56: this member started the session. Before anybody else can
|
|
107
|
+
* join it puts the base document - EXACTLY the bytes it opened - with
|
|
108
|
+
* `coedit.blob.put('base', bytes)`. A member that did not start it opens
|
|
109
|
+
* `coedit.blob.get('base')`, never the file: the file may hold a save of
|
|
110
|
+
* the session already, and the log applies to the base.
|
|
111
|
+
*/
|
|
112
|
+
created: boolean;
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
/** `coedit.join` params: join (or start) the session of an opened file. */
|
|
@@ -215,23 +244,58 @@ export declare interface FileInfo {
|
|
|
215
244
|
* `file` for a single encrypted file (`.fxe`). As the server says it;
|
|
216
245
|
* ABSENT for a file that is not encrypted.
|
|
217
246
|
*
|
|
218
|
-
* ⚠
|
|
219
|
-
*
|
|
220
|
-
* (`failed` with `encrypted`), a save is refused, and `readOnly`
|
|
221
|
-
*
|
|
222
|
-
* field is information, not a promise of plaintext.
|
|
247
|
+
* ⚠ The server holds such a file only as ciphertext and has no key. Unless
|
|
248
|
+
* `plaintext` is true, filex hands the app nothing of it: `file.read` is
|
|
249
|
+
* refused (`failed` with `encrypted`), a save is refused, and `readOnly`
|
|
250
|
+
* is true.
|
|
223
251
|
*/
|
|
224
252
|
encrypted?: 'folder' | 'vault' | 'file';
|
|
253
|
+
/**
|
|
254
|
+
* 0.56.0: filex hands this encrypted file to the app in the clear. The
|
|
255
|
+
* person's browser, which holds the folder key while the folder is
|
|
256
|
+
* unlocked, decrypts it for `file.read` and encrypts what `file.save`
|
|
257
|
+
* hands over before it leaves the browser; the server sees only
|
|
258
|
+
* ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app
|
|
259
|
+
* whose manifest says `encrypted_folders: {"open": true}` (the grant
|
|
260
|
+
* `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.
|
|
261
|
+
*
|
|
262
|
+
* In a vault (`encrypted: 'vault'`) the browser reads the file from the
|
|
263
|
+
* vault's packs and writes a save as the vault's next generation, under
|
|
264
|
+
* the vault's write lock (one writer at a time). A save is refused -
|
|
265
|
+
* `failed` with `vault_locked` - while somebody else writes the vault, and
|
|
266
|
+
* with `vault_lock_lost` when this session's write lock ended before the
|
|
267
|
+
* save was committed: nothing was written, save again. The vault locked
|
|
268
|
+
* meanwhile (its keys left the browser): `unavailable`, and filex closes
|
|
269
|
+
* the app.
|
|
270
|
+
*
|
|
271
|
+
* What changes for the app: a read and a save are held whole in the page
|
|
272
|
+
* (at most LIMITS.maxEncryptedBytes); a save is written only over the file
|
|
273
|
+
* as it was opened (`failed` with `changed` when it changed meanwhile);
|
|
274
|
+
* `file.saveAs` answers `unavailable` (a copy elsewhere would leave the
|
|
275
|
+
* folder decrypted); the app's module (`engine.call`, `job.submit`) is
|
|
276
|
+
* never handed the file; `state.get` / `state.set` use a store of their
|
|
277
|
+
* own for this folder, sealed in the person's browser (the server keeps it
|
|
278
|
+
* and cannot read it; apart from the app's ordinary store, a little smaller
|
|
279
|
+
* as it is measured sealed). Everything the app holds is plaintext: keep it
|
|
280
|
+
* in the frame. In a folder whose names are encrypted too, `name` and
|
|
281
|
+
* `ext` are the decrypted name's - the server never sees them.
|
|
282
|
+
*/
|
|
283
|
+
plaintext?: true;
|
|
225
284
|
}
|
|
226
285
|
|
|
227
286
|
export declare interface FilexApp {
|
|
228
287
|
readonly session: Session;
|
|
229
288
|
/** The file at `index` (default the first) the interface was opened with. */
|
|
230
289
|
open(index?: number): Promise<OpenedFile>;
|
|
231
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* Save new content over an opened file. `through` (filex 0.56, editing
|
|
292
|
+
* together): the last log entry the saved document holds, so the session
|
|
293
|
+
* knows what is saved.
|
|
294
|
+
*/
|
|
232
295
|
save(data: SaveData, opts?: {
|
|
233
296
|
index?: number;
|
|
234
297
|
mime?: string;
|
|
298
|
+
through?: number;
|
|
235
299
|
}): Promise<SaveResult>;
|
|
236
300
|
/** Save a NEW file; filex asks the person where (its own folder picker). */
|
|
237
301
|
saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;
|
|
@@ -260,11 +324,13 @@ export declare interface FilexApp {
|
|
|
260
324
|
*/
|
|
261
325
|
print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;
|
|
262
326
|
/**
|
|
263
|
-
* Editing together (filex 0.
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
327
|
+
* Editing together (filex 0.56; the app's manifest says `"co_edit":
|
|
328
|
+
* {"open": true}`, granted as `files:co-edit`). filex seals what the app appends, orders it
|
|
329
|
+
* through its relay and hands every member the same entries in the same
|
|
330
|
+
* order (`coedit.entry` events after `subscribe`); the app never holds a
|
|
331
|
+
* key. `join` answering `unavailable` (filex 0.55, editing together off
|
|
332
|
+
* here, an encrypted file) means: edit alone. The member that started the
|
|
333
|
+
* session (`created`) puts the base document before anybody else may join.
|
|
268
334
|
*/
|
|
269
335
|
coedit: {
|
|
270
336
|
join(index?: number): Promise<CoEditHello>;
|
|
@@ -282,7 +348,12 @@ export declare interface FilexApp {
|
|
|
282
348
|
submit(action: string, params?: Record<string, unknown>): Promise<{
|
|
283
349
|
op: unknown;
|
|
284
350
|
}>;
|
|
285
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* This person's small store for this app (JSON values, 8 KiB each). While
|
|
353
|
+
* the app holds a file of an encrypted folder in the clear
|
|
354
|
+
* (`FileInfo.plaintext`, filex 0.56) it is another store, sealed in the
|
|
355
|
+
* browser - apart from this one, and empty while the folder is locked.
|
|
356
|
+
*/
|
|
286
357
|
state: {
|
|
287
358
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
288
359
|
set(key: string, value: unknown): Promise<void>;
|
|
@@ -338,9 +409,10 @@ export declare type HostEvent =
|
|
|
338
409
|
/** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */
|
|
339
410
|
| 'coedit.cursor'
|
|
340
411
|
/**
|
|
341
|
-
* Editing together: the host stopped handing entries over
|
|
342
|
-
*
|
|
343
|
-
*
|
|
412
|
+
* Editing together: the host stopped handing entries over. `data:
|
|
413
|
+
* CoEditDropped` - `gone`: the membership ended (join again); `broken`: an
|
|
414
|
+
* entry did not check out (stop editing together). A connection that drops
|
|
415
|
+
* for a moment is the host's own: it reconnects and says nothing.
|
|
344
416
|
*/
|
|
345
417
|
| 'coedit.dropped';
|
|
346
418
|
|
|
@@ -417,6 +489,13 @@ export declare const LIMITS: {
|
|
|
417
489
|
* is far smaller.
|
|
418
490
|
*/
|
|
419
491
|
readonly maxPrintBytes: number;
|
|
492
|
+
/**
|
|
493
|
+
* The largest file of an encrypted folder filex hands an app in the clear
|
|
494
|
+
* or takes back from it (filex 0.56, `FileInfo.plaintext`): it is
|
|
495
|
+
* decrypted and encrypted whole in the page, as filex's own explorer does
|
|
496
|
+
* for a file of this size.
|
|
497
|
+
*/
|
|
498
|
+
readonly maxEncryptedBytes: number;
|
|
420
499
|
};
|
|
421
500
|
|
|
422
501
|
/** What the app may ask the host. Anything else is `unknown_method`. */
|
|
@@ -426,10 +505,10 @@ export declare type Method = 'session.get' | 'file.read' | 'file.save' | 'file.s
|
|
|
426
505
|
/** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */
|
|
427
506
|
| 'ui.print'
|
|
428
507
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
508
|
+
* Editing together (`coedit.*`, task #189): the session's log the host
|
|
509
|
+
* seals, orders through filex's relay and opens again. Defined in filex
|
|
510
|
+
* 0.55, which answers each `unavailable`; offered from 0.56 to an app
|
|
511
|
+
* granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.
|
|
433
512
|
*/
|
|
434
513
|
| 'coedit.join' | 'coedit.subscribe' | 'coedit.append' | 'coedit.lease' | 'coedit.cursor' | 'coedit.blob.put' | 'coedit.blob.get' | 'coedit.leave';
|
|
435
514
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-app-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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
|
@@ -84,8 +84,12 @@ export interface FilexApp {
|
|
|
84
84
|
readonly session: Session;
|
|
85
85
|
/** The file at `index` (default the first) the interface was opened with. */
|
|
86
86
|
open(index?: number): Promise<OpenedFile>;
|
|
87
|
-
/**
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Save new content over an opened file. `through` (filex 0.56, editing
|
|
89
|
+
* together): the last log entry the saved document holds, so the session
|
|
90
|
+
* knows what is saved.
|
|
91
|
+
*/
|
|
92
|
+
save(data: SaveData, opts?: { index?: number; mime?: string; through?: number }): Promise<SaveResult>;
|
|
89
93
|
/** Save a NEW file; filex asks the person where (its own folder picker). */
|
|
90
94
|
saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;
|
|
91
95
|
/** Unsaved changes, or not: filex asks before the person leaves them. */
|
|
@@ -113,11 +117,13 @@ export interface FilexApp {
|
|
|
113
117
|
*/
|
|
114
118
|
print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;
|
|
115
119
|
/**
|
|
116
|
-
* Editing together (filex 0.
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
120
|
+
* Editing together (filex 0.56; the app's manifest says `"co_edit":
|
|
121
|
+
* {"open": true}`, granted as `files:co-edit`). filex seals what the app appends, orders it
|
|
122
|
+
* through its relay and hands every member the same entries in the same
|
|
123
|
+
* order (`coedit.entry` events after `subscribe`); the app never holds a
|
|
124
|
+
* key. `join` answering `unavailable` (filex 0.55, editing together off
|
|
125
|
+
* here, an encrypted file) means: edit alone. The member that started the
|
|
126
|
+
* session (`created`) puts the base document before anybody else may join.
|
|
121
127
|
*/
|
|
122
128
|
coedit: {
|
|
123
129
|
join(index?: number): Promise<CoEditHello>;
|
|
@@ -133,7 +139,12 @@ export interface FilexApp {
|
|
|
133
139
|
call<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
134
140
|
/** Queue one of the app's actions on the opened files; answers the op. */
|
|
135
141
|
submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;
|
|
136
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* This person's small store for this app (JSON values, 8 KiB each). While
|
|
144
|
+
* the app holds a file of an encrypted folder in the clear
|
|
145
|
+
* (`FileInfo.plaintext`, filex 0.56) it is another store, sealed in the
|
|
146
|
+
* browser - apart from this one, and empty while the folder is locked.
|
|
147
|
+
*/
|
|
137
148
|
state: {
|
|
138
149
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
139
150
|
set(key: string, value: unknown): Promise<void>;
|
|
@@ -333,7 +344,9 @@ function bridge(port: MessagePort, opts: ConnectOptions): FilexApp {
|
|
|
333
344
|
},
|
|
334
345
|
async save(data, o = {}) {
|
|
335
346
|
const p = await payload(data);
|
|
336
|
-
|
|
347
|
+
const params: Record<string, unknown> = { index: o.index ?? 0, data: p.data, mime: o.mime };
|
|
348
|
+
if (typeof o.through === 'number') params.through = o.through;
|
|
349
|
+
return request<SaveResult>('file.save', params, p.transfer);
|
|
337
350
|
},
|
|
338
351
|
async saveAs(name, data, mime) {
|
|
339
352
|
const p = await payload(data);
|
package/src/protocol.ts
CHANGED
|
@@ -56,10 +56,10 @@ export type Method =
|
|
|
56
56
|
/** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */
|
|
57
57
|
| 'ui.print'
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* Editing together (`coedit.*`, task #189): the session's log the host
|
|
60
|
+
* seals, orders through filex's relay and opens again. Defined in filex
|
|
61
|
+
* 0.55, which answers each `unavailable`; offered from 0.56 to an app
|
|
62
|
+
* granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.
|
|
63
63
|
*/
|
|
64
64
|
| 'coedit.join'
|
|
65
65
|
| 'coedit.subscribe'
|
|
@@ -115,9 +115,10 @@ export type HostEvent =
|
|
|
115
115
|
/** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */
|
|
116
116
|
| 'coedit.cursor'
|
|
117
117
|
/**
|
|
118
|
-
* Editing together: the host stopped handing entries over
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
* Editing together: the host stopped handing entries over. `data:
|
|
119
|
+
* CoEditDropped` - `gone`: the membership ended (join again); `broken`: an
|
|
120
|
+
* entry did not check out (stop editing together). A connection that drops
|
|
121
|
+
* for a moment is the host's own: it reconnects and says nothing.
|
|
121
122
|
*/
|
|
122
123
|
| 'coedit.dropped';
|
|
123
124
|
|
|
@@ -219,13 +220,43 @@ export interface FileInfo {
|
|
|
219
220
|
* `file` for a single encrypted file (`.fxe`). As the server says it;
|
|
220
221
|
* ABSENT for a file that is not encrypted.
|
|
221
222
|
*
|
|
222
|
-
* ⚠
|
|
223
|
-
*
|
|
224
|
-
* (`failed` with `encrypted`), a save is refused, and `readOnly`
|
|
225
|
-
*
|
|
226
|
-
* field is information, not a promise of plaintext.
|
|
223
|
+
* ⚠ The server holds such a file only as ciphertext and has no key. Unless
|
|
224
|
+
* `plaintext` is true, filex hands the app nothing of it: `file.read` is
|
|
225
|
+
* refused (`failed` with `encrypted`), a save is refused, and `readOnly`
|
|
226
|
+
* is true.
|
|
227
227
|
*/
|
|
228
228
|
encrypted?: 'folder' | 'vault' | 'file';
|
|
229
|
+
/**
|
|
230
|
+
* 0.56.0: filex hands this encrypted file to the app in the clear. The
|
|
231
|
+
* person's browser, which holds the folder key while the folder is
|
|
232
|
+
* unlocked, decrypts it for `file.read` and encrypts what `file.save`
|
|
233
|
+
* hands over before it leaves the browser; the server sees only
|
|
234
|
+
* ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app
|
|
235
|
+
* whose manifest says `encrypted_folders: {"open": true}` (the grant
|
|
236
|
+
* `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.
|
|
237
|
+
*
|
|
238
|
+
* In a vault (`encrypted: 'vault'`) the browser reads the file from the
|
|
239
|
+
* vault's packs and writes a save as the vault's next generation, under
|
|
240
|
+
* the vault's write lock (one writer at a time). A save is refused -
|
|
241
|
+
* `failed` with `vault_locked` - while somebody else writes the vault, and
|
|
242
|
+
* with `vault_lock_lost` when this session's write lock ended before the
|
|
243
|
+
* save was committed: nothing was written, save again. The vault locked
|
|
244
|
+
* meanwhile (its keys left the browser): `unavailable`, and filex closes
|
|
245
|
+
* the app.
|
|
246
|
+
*
|
|
247
|
+
* What changes for the app: a read and a save are held whole in the page
|
|
248
|
+
* (at most LIMITS.maxEncryptedBytes); a save is written only over the file
|
|
249
|
+
* as it was opened (`failed` with `changed` when it changed meanwhile);
|
|
250
|
+
* `file.saveAs` answers `unavailable` (a copy elsewhere would leave the
|
|
251
|
+
* folder decrypted); the app's module (`engine.call`, `job.submit`) is
|
|
252
|
+
* never handed the file; `state.get` / `state.set` use a store of their
|
|
253
|
+
* own for this folder, sealed in the person's browser (the server keeps it
|
|
254
|
+
* and cannot read it; apart from the app's ordinary store, a little smaller
|
|
255
|
+
* as it is measured sealed). Everything the app holds is plaintext: keep it
|
|
256
|
+
* in the frame. In a folder whose names are encrypted too, `name` and
|
|
257
|
+
* `ext` are the decrypted name's - the server never sees them.
|
|
258
|
+
*/
|
|
259
|
+
plaintext?: true;
|
|
229
260
|
}
|
|
230
261
|
|
|
231
262
|
/** What `session.get` answers — what the app knows about where it runs. */
|
|
@@ -325,7 +356,7 @@ export interface PrintResult {
|
|
|
325
356
|
size: number;
|
|
326
357
|
}
|
|
327
358
|
|
|
328
|
-
/* ── editing together (coedit
|
|
359
|
+
/* ── editing together (coedit.*: filex 0.55 defined, 0.56 offers) ──────── */
|
|
329
360
|
|
|
330
361
|
/** What a member appends: the editor's changes, a lock request, a release. */
|
|
331
362
|
export type CoEditKind = 'changes' | 'lock' | 'release';
|
|
@@ -354,18 +385,46 @@ export interface CoEditJoinParams {
|
|
|
354
385
|
* What `coedit.join` answers: who the app is in the session and how far
|
|
355
386
|
* the log has gone. The entries come as `coedit.entry` events once the app
|
|
356
387
|
* subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`
|
|
357
|
-
* when filex offers no editing together here (filex 0.55,
|
|
358
|
-
*
|
|
388
|
+
* when filex offers no editing together here (filex 0.55, editing together
|
|
389
|
+
* turned off, an encrypted file, a vault): the app edits alone - the message
|
|
390
|
+
* says why (`not_ready` after waiting for a session's creator, `encrypted`,
|
|
391
|
+
* `nothing_to_watch` for a person who may only read the file while nobody
|
|
392
|
+
* edits it, or the server's code: `coedit_off`, `coedit_build`,
|
|
393
|
+
* `coedit_file_changed`).
|
|
359
394
|
*/
|
|
360
395
|
export interface CoEditHello {
|
|
361
396
|
session: string;
|
|
362
397
|
me: CoEditMember;
|
|
363
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* The last entry's seq when filex handed the session over (0: an empty
|
|
400
|
+
* log). filex 0.56: the changes this browser kept from an earlier opening
|
|
401
|
+
* of the file (a lost connection, a closed tab), which filex sends before
|
|
402
|
+
* it answers, are in the log up to here - an editor that starts once it
|
|
403
|
+
* has read this far opens with them.
|
|
404
|
+
*/
|
|
364
405
|
head: number;
|
|
365
|
-
/** The last `changes` entry's seq
|
|
406
|
+
/** The last `changes` entry's seq, as far as `head`. */
|
|
366
407
|
changesHead: number;
|
|
367
408
|
/** The last entry the file on the storage holds. */
|
|
368
409
|
savedThrough: number;
|
|
410
|
+
/**
|
|
411
|
+
* filex 0.56: this member started the session. Before anybody else can
|
|
412
|
+
* join it puts the base document - EXACTLY the bytes it opened - with
|
|
413
|
+
* `coedit.blob.put('base', bytes)`. A member that did not start it opens
|
|
414
|
+
* `coedit.blob.get('base')`, never the file: the file may hold a save of
|
|
415
|
+
* the session already, and the log applies to the base.
|
|
416
|
+
*/
|
|
417
|
+
created: boolean;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** `coedit.dropped` event data (filex 0.56). */
|
|
421
|
+
export interface CoEditDropped {
|
|
422
|
+
/** The last entry the host handed over. */
|
|
423
|
+
from: number;
|
|
424
|
+
/** `gone`: the membership ended (the session closed, or the page was away
|
|
425
|
+
* too long) - join again. `broken`: an entry did not check out (missing,
|
|
426
|
+
* moved, altered, replayed) - stop editing together. */
|
|
427
|
+
reason: 'gone' | 'broken';
|
|
369
428
|
}
|
|
370
429
|
|
|
371
430
|
/** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */
|
|
@@ -396,7 +455,8 @@ export interface CoEditLeaseResult {
|
|
|
396
455
|
granted: boolean;
|
|
397
456
|
}
|
|
398
457
|
|
|
399
|
-
/** `coedit.cursor` params: passed to the other members, not kept.
|
|
458
|
+
/** `coedit.cursor` params: passed to the other members, not kept. The host
|
|
459
|
+
* sends a few a second at most (the last one wins). */
|
|
400
460
|
export interface CoEditCursorParams {
|
|
401
461
|
cursor: unknown;
|
|
402
462
|
}
|
|
@@ -523,6 +583,13 @@ export const LIMITS = {
|
|
|
523
583
|
* is far smaller.
|
|
524
584
|
*/
|
|
525
585
|
maxPrintBytes: 64 << 20,
|
|
586
|
+
/**
|
|
587
|
+
* The largest file of an encrypted folder filex hands an app in the clear
|
|
588
|
+
* or takes back from it (filex 0.56, `FileInfo.plaintext`): it is
|
|
589
|
+
* decrypted and encrypted whole in the page, as filex's own explorer does
|
|
590
|
+
* for a file of this size.
|
|
591
|
+
*/
|
|
592
|
+
maxEncryptedBytes: 200 << 20,
|
|
526
593
|
} as const;
|
|
527
594
|
|
|
528
595
|
/* ── guards ────────────────────────────────────────────────────────────── */
|