@atoapayments/pay-host
v0.0.1
Published
Host Atoa checkout inside your own UI — a chat thread, a support widget, a nested iframe. Detects the frame environment it landed in, picks the tier that can actually complete a payment there, and tells you exactly why. A payment is always completable, an
Readme
@atoapayments/pay-host
Host Atoa checkout inside your own UI — a chat thread, a support widget, a nested iframe.
@atoapayments/pay-embed mounts a checkout. This package answers the question that comes first: given the
frame I actually landed in, what can work here? It detects the environment, picks the tier that can
complete a payment, degrades automatically when it must, and tells you exactly why.
npm i @atoapayments/pay-hostOr with no install — a script tag. @atoapayments/pay-embed is bundled into this file, so one tag is
the whole integration. @0 floats within the 0.x line; in production pin an exact version.
<script src="https://unpkg.com/@atoapayments/pay-host@0/dist/index.global.js"></script>
<script>
const { AtoaHost } = window.AtoaHost; // the tag exposes the package's exports as `window.AtoaHost`
// AtoaHost.mount(result, { … }) exactly as below
</script>import { AtoaHost } from '@atoapayments/pay-host';
const handle = AtoaHost.mount(result, {
container: document.querySelector('#checkout'),
environment: 'sandbox',
preset: 'bubble', // chat-width defaults
hosted: { // the bottom rung, so a payment is always completable
url: result.paymentUrl,
qrUrl: result.qrCodeUrl,
amountDisplay: '£12.50',
payee: 'Liberty London',
},
// Outcomes are resource.event domain events (see @atoapayments/pay-embed). UX signals only —
// confirm by polling (`payment.get` / `awaitSettled`) before fulfilling.
onPaymentCompleted: (e) => confirmOrder(e.data),
onEvent: (e) => log(e.type), // every domain event, including future types
});
handle.decision.tier; // 'embed' | 'embed-relay' | 'embed-copy' | 'hosted-link'
handle.decision.reason; // why, in a sentence you can put in a logDetect · degrade · diagnose
Detect. Before mounting, the SDK reads its situation: the real (possibly opaque) origin, sandbox flags, delegated permissions, storage, popup capability, nesting depth.
Degrade. One decision picks the tier. Nothing here is the integrator's homework:
| Tier | When | What the customer sees |
|---|---|---|
| embed | A normal embedding context | The full checkout |
| embed-relay | Nested inside another frame | The full checkout; height relayed to the page above |
| embed-copy | A sandbox above blocks popups | The checkout, with the hop as a copyable link + QR — because a bank link would fail silently here |
| hosted-link | Opaque origin (sandbox without allow-same-origin) | A payment card with the hosted link, instead of a frame that cannot work |
Diagnose. AtoaHost.diagnose() returns a machine-readable report — sandbox flags, allow-chain
results, ancestor origins, storage state, the chosen tier and why, plus warnings that each name the
attribute to change. ?atoaDebug=1 renders the same thing as an overlay. It carries no payment data,
so it is safe to paste into a ticket.
The integrator contract
INTEGRATORS.md is the whole thing: the exact allow string, the sandbox flags
and what breaks without each, the relay snippet, and the hop adapter. There is no origin to
register — framing is open by design, and the security boundary is the server-minted payment
plus the customer's SCA on Atoa's origin, not who framed the page.
Conformance
npx @atoapayments/pay-host conformance https://your-page.exampleLoads your page headlessly and checks everything in the contract: frame present, real origin, sandbox
flags sufficient, permissions delegated, no payment data posted to '*', height applied rather than
fixed, relay wired when nested. Exit 0 means you are done. --json for CI.
The reference rig
npm run rig # A (merchant) → B (chatbot) → C (checkout), three real origins
npm run proof:rig # the same chain driven headlessly across the sandbox permutationsexamples/nested-host/ is a runnable two-level chain you can copy from, and the harness that proves
the nested-frame behaviour rather than asserting it. Flags let you reproduce each hostile case:
--sandbox "…", --no-webauthn, --no-referrer.
It is also how we found that location.origin lies inside an opaque-origin sandbox (it reports
the URL's tuple origin) while window.origin tells the truth. The detector probes the latter.
What this package does not do
It does not create payments, define new message types, or weaken any origin rule — the postMessage
bridge stays pinned to the exact Atoa origin that answered the handshake. pay-embed keeps exactly
two seams for this package to use (onHop, relay) and nothing else moved into it.
Gates
npm test # 45 tests
npm run typecheck
npm run build
npm run proof:rig # 28 checks in a real browser across five frame configurations