@brftech/filex-app-ui 0.51.0 → 0.52.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 +1 -0
- 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 +30 -24
- package/dist/filex-app-ui.js.map +1 -1
- package/dist/index.d.ts +34 -1
- package/package.json +1 -1
- package/src/index.ts +14 -0
- package/src/protocol.ts +27 -1
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ with a `<script src>`: it defines `window.FilexAppUI.connect`.
|
|
|
35
35
|
| `fx.call(method, params)` | ask the app's own module (`ui_call`) |
|
|
36
36
|
| `fx.submit(action, params)` | queue one of the app's actions on the opened files - like `copy`, from a click, or filex asks |
|
|
37
37
|
| `fx.state.get(key)` / `fx.state.set(key, value)` | a small store for this app and this person |
|
|
38
|
+
| `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 |
|
|
38
39
|
| `fx.on('theme' \| 'locale' \| 'file.changed' \| 'close.request' \| 'app.updated', fn)` | listen to filex |
|
|
39
40
|
|
|
40
41
|
`connect()` paints filex's colours (`--fe-*` custom properties), `lang`, `dir`
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var FilexAppUI=function(o){"use strict";const S="filex:hello",O="filex:port",L=["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"],h={maxTextBytes:33554432,maxStateBytes:8192,maxLineChars:300,connectTimeoutMs:1e4,maxDownloadBytes:268435456};function x(n){return!!n&&typeof n=="object"&&n.type===S&&typeof n.v=="number"}function R(n){return!!n&&typeof n=="object"&&n.type===O}function B(n){return!!n&&typeof n=="object"&&typeof n.id=="number"&&typeof n.method=="string"}function I(n){return L.includes(n)}class p extends Error{constructor(u){super(u.message||u.code),this.name="FilexError",this.code=u.code}}let E=null;function T(n={}){return E||(E=k(n)),E}function k(n){return new Promise((u,y)=>{if(typeof window>"u"||window.parent===window){y(new p({code:"unavailable",message:"not running inside filex"}));return}const a=window.parent,l=D(),c=setTimeout(()=>{window.removeEventListener("message",w),y(new p({code:"unavailable",message:"filex did not answer"}))},n.timeoutMs??h.connectTimeoutMs);function w(m){if(m.source!==a||!R(m.data)||!m.ports[0]||l&&m.origin!==l)return;window.removeEventListener("message",w),clearTimeout(c);const
|
|
1
|
+
var FilexAppUI=function(o){"use strict";const S="filex:hello",O="filex:port",L=["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"],h={maxTextBytes:33554432,maxStateBytes:8192,maxLineChars:300,connectTimeoutMs:1e4,maxDownloadBytes:268435456};function x(n){return!!n&&typeof n=="object"&&n.type===S&&typeof n.v=="number"}function R(n){return!!n&&typeof n=="object"&&n.type===O}function B(n){return!!n&&typeof n=="object"&&typeof n.id=="number"&&typeof n.method=="string"}function I(n){return L.includes(n)}class p extends Error{constructor(u){super(u.message||u.code),this.name="FilexError",this.code=u.code}}let E=null;function T(n={}){return E||(E=k(n)),E}function k(n){return new Promise((u,y)=>{if(typeof window>"u"||window.parent===window){y(new p({code:"unavailable",message:"not running inside filex"}));return}const a=window.parent,l=D(),c=setTimeout(()=>{window.removeEventListener("message",w),y(new p({code:"unavailable",message:"filex did not answer"}))},n.timeoutMs??h.connectTimeoutMs);function w(m){if(m.source!==a||!R(m.data)||!m.ports[0]||l&&m.origin!==l)return;window.removeEventListener("message",w),clearTimeout(c);const r=m.ports[0],f=H(r,n);f.request("session.get").then(g=>{f.session=g,n.applyTheme!==!1&&(M(g.theme,g.locale,g.dir),f.on("theme",d=>M(d)),f.on("locale",d=>{const e=d;M(void 0,e.locale,e.dir)})),u(f)}).catch(y)}window.addEventListener("message",w),a.postMessage({type:S,v:1},"*")})}function D(){try{const n=window.location.ancestorOrigins;if(n&&n.length>0&&n[0]!=="null")return n[0]}catch{}return""}function H(n,u){let y=0;const a=new Map,l=new Map;let c=null;n.onmessage=e=>{const t=e.data;if(!(!t||typeof t!="object")){if(typeof t.id=="number"){const s=a.get(t.id);if(!s)return;a.delete(t.id),t.error?s.reject(new p(t.error)):s.resolve(t.result);return}if(typeof t.hid=="number"){w(t);return}if(typeof t.event=="string")for(const s of l.get(t.event)??[])try{s(t.data)}catch(i){console.error("[filex-app-ui] listener failed",i)}}};async function w(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 m();n.postMessage({hid:e.hid,result:t??{saved:!0}})}catch(t){const s=t instanceof p?{code:t.code,message:t.message}:{code:"failed",message:String(t?.message??t)};n.postMessage({hid:e.hid,error:s})}return}n.postMessage({hid:e.hid,error:{code:"unknown_method"}})}async function m(){if(!c)return null;const e=await c();return e==null?null:d.save(e)}function r(e,t,s=[]){const i=++y;return new Promise((b,v)=>{a.set(i,{resolve:b,reject:v});try{n.postMessage({id:i,method:e,params:t},s)}catch(P){a.delete(i),v(P)}})}function f(e,t){r(e,t).catch(s=>console.warn("[filex-app-ui]",e,s))}async function g(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 d={session:void 0,async open(e=0){const t=d.session?.files?.[e];if(!t)throw new p({code:"not_found",message:`no file at ${e}`});const s=(i,b)=>r("file.read",{index:e,as:i}).then(v=>v[b]);return{...t,text:()=>s("text","text"),bytes:()=>s("bytes","bytes"),stream:()=>s("stream","stream"),save:(i,b)=>d.save(i,{index:e,mime:b})}},async save(e,t={}){const s=await g(e);return r("file.save",{index:t.index??0,data:s.data,mime:t.mime},s.transfer)},async saveAs(e,t,s){const i=await g(t);return r("file.saveAs",{name:e,data:i.data,mime:s},i.transfer)},async download(e,t,s){const i=await g(t);return r("ui.download",{name:e,data:i.data,mime:s},i.transfer)},dirty(e){f("ui.dirty",{dirty:!!e})},title(e){f("ui.title",{text:String(e).slice(0,h.maxLineChars)})},toast(e,t){f("ui.toast",{text:String(e).slice(0,h.maxLineChars),tone:t})},confirm(e){return r("ui.confirm",typeof e=="string"?{text:e}:e)},close(){f("ui.close")},copy(e){return r("clipboard.write",{text:String(e)})},call(e,t){return r("engine.call",{method:e,params:t})},submit(e,t){return r("job.submit",{action:e,params:t})},state:{get(e){return r("state.get",{key:e})},set(e,t){return r("state.set",{key:e,value:t})}},license:{get(){return r("license.get")}},on(e,t){let s=l.get(e);return s||l.set(e,s=new Set),s.add(t),()=>s.delete(t)},onSave(e){return c=e,()=>{c===e&&(c=null)}},request:r};return u.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(),m().catch(t=>d.toast(String(t?.message??t),"error")))},!0),d}function M(n,u,y){if(typeof document>"u")return;const a=document.documentElement;if(n){a.dataset.theme=n.mode,a.style.colorScheme=n.mode;for(const[l,c]of Object.entries(n.tokens??{}))/^--fe-[a-z0-9-]+$/.test(l)&&typeof c=="string"&&a.style.setProperty(l,c)}u&&(a.lang=u),y&&(a.dir=y)}return o.BRIDGE_VERSION=1,o.FilexError=p,o.HELLO=S,o.LIMITS=h,o.METHODS=L,o.PORT=O,o.connect=T,o.isHello=x,o.isKnownMethod=I,o.isPortMessage=R,o.isRequest=B,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\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];\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\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\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\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 * `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/** `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} 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} 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 /** 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 /** 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 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 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","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\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];\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\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\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\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 * `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} 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} 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 /** 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 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","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
|
@@ -13,7 +13,8 @@ const k = 1, S = "filex:hello", E = "filex:port", L = [
|
|
|
13
13
|
"engine.call",
|
|
14
14
|
"job.submit",
|
|
15
15
|
"state.get",
|
|
16
|
-
"state.set"
|
|
16
|
+
"state.set",
|
|
17
|
+
"license.get"
|
|
17
18
|
], x = {
|
|
18
19
|
/** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */
|
|
19
20
|
maxTextBytes: 33554432,
|
|
@@ -59,7 +60,7 @@ function O(n) {
|
|
|
59
60
|
function g(y) {
|
|
60
61
|
if (y.source !== i || !B(y.data) || !y.ports[0] || u && y.origin !== u) return;
|
|
61
62
|
window.removeEventListener("message", g), clearTimeout(a);
|
|
62
|
-
const
|
|
63
|
+
const r = y.ports[0], f = T(r, n);
|
|
63
64
|
f.request("session.get").then((m) => {
|
|
64
65
|
f.session = m, n.applyTheme !== !1 && (v(m.theme, m.locale, m.dir), f.on("theme", (l) => v(l)), f.on("locale", (l) => {
|
|
65
66
|
const e = l;
|
|
@@ -99,8 +100,8 @@ function T(n, c) {
|
|
|
99
100
|
for (const s of u.get(t.event) ?? [])
|
|
100
101
|
try {
|
|
101
102
|
s(t.data);
|
|
102
|
-
} catch (
|
|
103
|
-
console.error("[filex-app-ui] listener failed",
|
|
103
|
+
} catch (o) {
|
|
104
|
+
console.error("[filex-app-ui] listener failed", o);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
};
|
|
@@ -126,19 +127,19 @@ function T(n, c) {
|
|
|
126
127
|
const e = await a();
|
|
127
128
|
return e == null ? null : l.save(e);
|
|
128
129
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
130
|
+
function r(e, t, s = []) {
|
|
131
|
+
const o = ++d;
|
|
131
132
|
return new Promise((w, h) => {
|
|
132
|
-
i.set(
|
|
133
|
+
i.set(o, { resolve: w, reject: h });
|
|
133
134
|
try {
|
|
134
|
-
n.postMessage({ id:
|
|
135
|
+
n.postMessage({ id: o, method: e, params: t }, s);
|
|
135
136
|
} catch (M) {
|
|
136
|
-
i.delete(
|
|
137
|
+
i.delete(o), h(M);
|
|
137
138
|
}
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
function f(e, t) {
|
|
141
|
-
|
|
142
|
+
r(e, t).catch((s) => console.warn("[filex-app-ui]", e, s));
|
|
142
143
|
}
|
|
143
144
|
async function m(e) {
|
|
144
145
|
if (typeof e == "string") return { data: e, transfer: [] };
|
|
@@ -160,26 +161,26 @@ function T(n, c) {
|
|
|
160
161
|
async open(e = 0) {
|
|
161
162
|
const t = l.session?.files?.[e];
|
|
162
163
|
if (!t) throw new p({ code: "not_found", message: `no file at ${e}` });
|
|
163
|
-
const s = (
|
|
164
|
+
const s = (o, w) => r("file.read", { index: e, as: o }).then((h) => h[w]);
|
|
164
165
|
return {
|
|
165
166
|
...t,
|
|
166
167
|
text: () => s("text", "text"),
|
|
167
168
|
bytes: () => s("bytes", "bytes"),
|
|
168
169
|
stream: () => s("stream", "stream"),
|
|
169
|
-
save: (
|
|
170
|
+
save: (o, w) => l.save(o, { index: e, mime: w })
|
|
170
171
|
};
|
|
171
172
|
},
|
|
172
173
|
async save(e, t = {}) {
|
|
173
174
|
const s = await m(e);
|
|
174
|
-
return
|
|
175
|
+
return r("file.save", { index: t.index ?? 0, data: s.data, mime: t.mime }, s.transfer);
|
|
175
176
|
},
|
|
176
177
|
async saveAs(e, t, s) {
|
|
177
|
-
const
|
|
178
|
-
return
|
|
178
|
+
const o = await m(t);
|
|
179
|
+
return r("file.saveAs", { name: e, data: o.data, mime: s }, o.transfer);
|
|
179
180
|
},
|
|
180
181
|
async download(e, t, s) {
|
|
181
|
-
const
|
|
182
|
-
return
|
|
182
|
+
const o = await m(t);
|
|
183
|
+
return r("ui.download", { name: e, data: o.data, mime: s }, o.transfer);
|
|
183
184
|
},
|
|
184
185
|
dirty(e) {
|
|
185
186
|
f("ui.dirty", { dirty: !!e });
|
|
@@ -191,26 +192,31 @@ function T(n, c) {
|
|
|
191
192
|
f("ui.toast", { text: String(e).slice(0, x.maxLineChars), tone: t });
|
|
192
193
|
},
|
|
193
194
|
confirm(e) {
|
|
194
|
-
return
|
|
195
|
+
return r("ui.confirm", typeof e == "string" ? { text: e } : e);
|
|
195
196
|
},
|
|
196
197
|
close() {
|
|
197
198
|
f("ui.close");
|
|
198
199
|
},
|
|
199
200
|
copy(e) {
|
|
200
|
-
return
|
|
201
|
+
return r("clipboard.write", { text: String(e) });
|
|
201
202
|
},
|
|
202
203
|
call(e, t) {
|
|
203
|
-
return
|
|
204
|
+
return r("engine.call", { method: e, params: t });
|
|
204
205
|
},
|
|
205
206
|
submit(e, t) {
|
|
206
|
-
return
|
|
207
|
+
return r("job.submit", { action: e, params: t });
|
|
207
208
|
},
|
|
208
209
|
state: {
|
|
209
210
|
get(e) {
|
|
210
|
-
return
|
|
211
|
+
return r("state.get", { key: e });
|
|
211
212
|
},
|
|
212
213
|
set(e, t) {
|
|
213
|
-
return
|
|
214
|
+
return r("state.set", { key: e, value: t });
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
license: {
|
|
218
|
+
get() {
|
|
219
|
+
return r("license.get");
|
|
214
220
|
}
|
|
215
221
|
},
|
|
216
222
|
on(e, t) {
|
|
@@ -222,7 +228,7 @@ function T(n, c) {
|
|
|
222
228
|
a === e && (a = null);
|
|
223
229
|
};
|
|
224
230
|
},
|
|
225
|
-
request:
|
|
231
|
+
request: r
|
|
226
232
|
};
|
|
227
233
|
return c.saveShortcut !== !1 && typeof document < "u" && document.addEventListener(
|
|
228
234
|
"keydown",
|
package/dist/filex-app-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set';\n\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];\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\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\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\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 * `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/** `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} 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} 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 /** 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 /** 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 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 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","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","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get';\n\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];\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\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\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\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 * `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} 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} 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 /** 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 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","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","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -131,6 +131,14 @@ export declare interface FilexApp {
|
|
|
131
131
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
132
132
|
set(key: string, value: unknown): Promise<void>;
|
|
133
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* The app's license (filex 0.52.0; a paid app installed from a store):
|
|
136
|
+
* `{status: "free"}` for a free app, `{status: "valid", valid_until,
|
|
137
|
+
* updates_until}` for a licensed one. Never the key, never the licensee.
|
|
138
|
+
*/
|
|
139
|
+
license: {
|
|
140
|
+
get(): Promise<LicenseInfo>;
|
|
141
|
+
};
|
|
134
142
|
/** Listen to filex. Returns the unsubscribe. */
|
|
135
143
|
on(event: HostEvent, handler: (data: unknown) => void): () => void;
|
|
136
144
|
/**
|
|
@@ -202,6 +210,29 @@ export declare interface JobSubmitParams {
|
|
|
202
210
|
params?: Record<string, unknown>;
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
/**
|
|
214
|
+
* `license.get` (filex 0.52.0): what the app's license says. A paid app is
|
|
215
|
+
* installed from a store, which issues its license and is asked about it every
|
|
216
|
+
* day; filex keeps the key and never hands it to the app. `status`:
|
|
217
|
+
*
|
|
218
|
+
* - `free`: not a paid app;
|
|
219
|
+
* - `valid`: the store said valid at its last check;
|
|
220
|
+
* - `grace`: valid, but the store could not be asked since - it holds until
|
|
221
|
+
* the grace the store signed ends;
|
|
222
|
+
* - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,
|
|
223
|
+
* `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held
|
|
224
|
+
* and its interface is not served, so an app rarely reads one of these.
|
|
225
|
+
*
|
|
226
|
+
* `valid_until` / `updates_until` are there for a valid license (dates as
|
|
227
|
+
* RFC 3339). Who holds the license is not the app's to read. filex 0.51.0
|
|
228
|
+
* and older answer `unknown_method`.
|
|
229
|
+
*/
|
|
230
|
+
export declare interface LicenseInfo {
|
|
231
|
+
status: string;
|
|
232
|
+
valid_until?: string;
|
|
233
|
+
updates_until?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
205
236
|
export declare const LIMITS: {
|
|
206
237
|
/** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */
|
|
207
238
|
readonly maxTextBytes: number;
|
|
@@ -216,7 +247,9 @@ export declare const LIMITS: {
|
|
|
216
247
|
};
|
|
217
248
|
|
|
218
249
|
/** What the app may ask the host. Anything else is `unknown_method`. */
|
|
219
|
-
export declare type Method = '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'
|
|
250
|
+
export declare type Method = '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'
|
|
251
|
+
/** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */
|
|
252
|
+
| 'license.get';
|
|
220
253
|
|
|
221
254
|
export declare const METHODS: readonly Method[];
|
|
222
255
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-app-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type Theme,
|
|
36
36
|
type ToastParams,
|
|
37
37
|
type DownloadResult,
|
|
38
|
+
type LicenseInfo,
|
|
38
39
|
} from './protocol';
|
|
39
40
|
|
|
40
41
|
export * from './protocol';
|
|
@@ -105,6 +106,14 @@ export interface FilexApp {
|
|
|
105
106
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
106
107
|
set(key: string, value: unknown): Promise<void>;
|
|
107
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* The app's license (filex 0.52.0; a paid app installed from a store):
|
|
111
|
+
* `{status: "free"}` for a free app, `{status: "valid", valid_until,
|
|
112
|
+
* updates_until}` for a licensed one. Never the key, never the licensee.
|
|
113
|
+
*/
|
|
114
|
+
license: {
|
|
115
|
+
get(): Promise<LicenseInfo>;
|
|
116
|
+
};
|
|
108
117
|
/** Listen to filex. Returns the unsubscribe. */
|
|
109
118
|
on(event: HostEvent, handler: (data: unknown) => void): () => void;
|
|
110
119
|
/**
|
|
@@ -335,6 +344,11 @@ function bridge(port: MessagePort, opts: ConnectOptions): FilexApp {
|
|
|
335
344
|
return request<void>('state.set', { key, value });
|
|
336
345
|
},
|
|
337
346
|
},
|
|
347
|
+
license: {
|
|
348
|
+
get() {
|
|
349
|
+
return request<LicenseInfo>('license.get');
|
|
350
|
+
},
|
|
351
|
+
},
|
|
338
352
|
on(event, handler) {
|
|
339
353
|
let set = listeners.get(event);
|
|
340
354
|
if (!set) listeners.set(event, (set = new Set()));
|
package/src/protocol.ts
CHANGED
|
@@ -50,7 +50,9 @@ export type Method =
|
|
|
50
50
|
| 'engine.call'
|
|
51
51
|
| 'job.submit'
|
|
52
52
|
| 'state.get'
|
|
53
|
-
| 'state.set'
|
|
53
|
+
| 'state.set'
|
|
54
|
+
/** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */
|
|
55
|
+
| 'license.get';
|
|
54
56
|
|
|
55
57
|
export const METHODS: readonly Method[] = [
|
|
56
58
|
'session.get',
|
|
@@ -68,6 +70,7 @@ export const METHODS: readonly Method[] = [
|
|
|
68
70
|
'job.submit',
|
|
69
71
|
'state.get',
|
|
70
72
|
'state.set',
|
|
73
|
+
'license.get',
|
|
71
74
|
];
|
|
72
75
|
|
|
73
76
|
/** What the host tells the app, unasked. */
|
|
@@ -274,6 +277,29 @@ export interface ConfirmParams {
|
|
|
274
277
|
danger?: boolean;
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
/**
|
|
281
|
+
* `license.get` (filex 0.52.0): what the app's license says. A paid app is
|
|
282
|
+
* installed from a store, which issues its license and is asked about it every
|
|
283
|
+
* day; filex keeps the key and never hands it to the app. `status`:
|
|
284
|
+
*
|
|
285
|
+
* - `free`: not a paid app;
|
|
286
|
+
* - `valid`: the store said valid at its last check;
|
|
287
|
+
* - `grace`: valid, but the store could not be asked since - it holds until
|
|
288
|
+
* the grace the store signed ends;
|
|
289
|
+
* - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,
|
|
290
|
+
* `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held
|
|
291
|
+
* and its interface is not served, so an app rarely reads one of these.
|
|
292
|
+
*
|
|
293
|
+
* `valid_until` / `updates_until` are there for a valid license (dates as
|
|
294
|
+
* RFC 3339). Who holds the license is not the app's to read. filex 0.51.0
|
|
295
|
+
* and older answer `unknown_method`.
|
|
296
|
+
*/
|
|
297
|
+
export interface LicenseInfo {
|
|
298
|
+
status: string;
|
|
299
|
+
valid_until?: string;
|
|
300
|
+
updates_until?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
277
303
|
/** `engine.call`: the app's own module, `ui_call` export. */
|
|
278
304
|
export interface EngineCallParams {
|
|
279
305
|
method: string;
|