The loader is thin on purpose
It holds no tokens, makes no requests, and stores no state beyond the currently open session. Everything sensitive — authentication, funding, signing, the invest itself — happens on the Cesto origin inside the dialog iframe or the popup window. No tokens, cookies, or user data ever pass through your page.The publishable key
cesto_pk_… is public by design, like a Stripe publishable key. It ships in your HTML,
anyone can read it, and that is fine — because it grants nothing on its own:
- Partner branding and attribution activate only after the backend validates the key
against the origin the request came from (
onVerified). - Every invest requires the end user’s own Cesto session plus a per-action signed challenge. A copied key cannot move anyone’s money.
- Framing is gated by the key: the invest page’s
frame-ancestorsis computed from it, so only your registered origins can frame the dialog. Someone who lifts your key onto their own site gets an unframeable page.
Pinned message channel
The loader accepts messages only from the exact window it opened — the dialog iframe’scontentWindow, or the popup window — and only from the Cesto origin. It sends its
init message to that explicit origin, never '*'.
That closes the obvious embedding attacks: a sibling iframe on your page cannot pose as the
dialog to hijack the session, and a page on another origin cannot inject events into your
callbacks.
The popup’s window name also carries an unguessable random suffix per loader instance, so
another script on your page cannot window.open(evil, 'cesto-invest') to take over an open
popup by its name.
Origin lockdown on baseUrl
baseUrl only accepts a cesto.co origin over HTTPS (or localhost / 127.0.0.1 for
development). Anything else throws at construction.
This matters most for the script-tag path: a data-cesto-base-url injected through a
compromised CMS field can’t reroute the flow — and with it, the user’s wallet prompts — to
an attacker-controlled origin. A rejected value logs
[Cesto] could not open the invest flow and the button does nothing, rather than silently
navigating somewhere.
The wallet relay
In dialog mode the invest app cannot reach browser wallet extensions itself — extensions don’t inject providers into cross-origin iframes — so it asks the SDK, running top-level on your page, to relay four things:- Detect installed wallet providers.
- Request a connection.
- Request a signature over a login challenge.
- Request a signature over a transaction the app composed.
Why the iframe has no sandbox attribute
Deliberate. The dialog frames the trusted Cesto origin, and OAuth login popups opened from
inside it have to work. The protection is on the server side instead: the invest page only
allows framing (frame-ancestors) for origins on the key’s allowlist, so the frame can
only ever be created by a site you registered.
Protocol versioning
Every message between the loader and the app is a versioned envelope ({ cesto: 1, type, payload }). Unknown versions and unknown message types are dropped,
never errored, so an older loader cannot be broken by a newer app, or vice versa.
Practically: pin the SDK version you ship, and expect forward compatibility rather than
relying on it.
What you should do on your side
1
Register exactly the origins you use
Including staging and preview deployments. Unregistered origins run unbranded and can’t
frame the dialog.
2
Serve the embedding page over HTTPS
Wallet prompts and OAuth flows on your origin deserve the same protection as the Cesto
origin has.
3
Pin a version in the CDN URL
@cesto/web-sdk@0.1.0 rather than a floating tag, so a release can’t change the flow
on a page you aren’t watching.4
Don't treat onClose as an outcome
It is not an authorization signal and not a confirmation. Only
onSuccess means an
invest completed — and even then, verify server-side before granting anything of value
on your side.Related
API keys
Publishable vs secret, scopes, and the origin allowlist.
Cesto security
How custody, signing, and execution work in the product itself.