Embedding OpenDoc
The same editor, embedded twice on one page: once in a plain
<iframe> a host wires itself, and once through the
<opendoc-editor> custom element. Both resolve their
capabilities from one authority before the frame's first navigation,
so neither can be talked out of the mode it was given — and each panel
prints the code a host writes to get it, which is the only real
difference between them.
- — roles
- — capabilities
- — mounts on this page
- — transports each
A plain iframe <iframe>
The host builds the URL and the sandbox by hand. Every parameter below is a decision this page had to make for itself.
- —
- — of — capabilities
What the frame is given
- Frame sandbox
- —
The editor mounts here
Nothing boots until you ask. Each editor holds a WebAssembly instance of roughly 100 MB, and a page that starts two of them on load costs a visitor 200 MB for a demo they may not have wanted — the same reason the home page's live demo is click-to-activate.
The same session, driven two ways. In process reaches
frame.contentWindow.opendoc directly, which a host can only
do same-origin. postMessage goes over the wire through
createHostClient, which works across origins once the
deployment names them. Every command, every event and every refusal on
both paths comes from one schema.
Result
—
Events
The custom element <opendoc-editor>
The same configuration, declared. The element resolves the role and
both withhold lists, derives the iframe sandbox from them, and
reports all of it back on opendoc-capabilities.
- —
- — of — capabilities
What the frame is given
- Frame sandbox
- —
The editor mounts here
Nothing boots until you ask. Each editor holds a WebAssembly instance of roughly 100 MB, and a page that starts two of them on load costs a visitor 200 MB for a demo they may not have wanted — the same reason the home page's live demo is click-to-activate.
The same session, driven two ways. In process reaches
frame.contentWindow.opendoc directly, which a host can only
do same-origin. postMessage goes over the wire through
createHostClient, which works across origins once the
deployment names them. Every command, every event and every refusal on
both paths comes from one schema.
Result
—
Events
What each layer actually enforces
The browser refuses a download the sandbox did not allow. The engine
refuses a mutation in Viewing mode at a single fail-closed choke point,
so a chrome defeated from devtools still meets it. And the chrome
disables a withheld capability with a reason rather than
hiding it — a control that vanishes teaches nothing, and a product that
silently lacks a feature is indistinguishable from one that is broken.
Open the command palette inside a readonly embed and look
at Open…: it is there, it is disabled, and it says why.
And the fourth layer: the API
A host that was not granted edit must not be able to run an
editing command through the SDK either, or the API is simply
the unlocked door beside three locked ones. So the capability gate runs
before dispatch: pick preview above, mount, and
run format.bold on either transport — the result is
{"ok": false, "refusal": {"code": "capability-withheld"}}
and the command never reaches the engine. Withhold
download and run file.export.docx for the same
answer. A refusal is a value, not just a toast.
To configure a container the other way round — every capability, every chrome region and the brand palette, with the configuration written out for you — use the SDK playground. The reference is Embed the editor.