Developers

The snippet

What embed.js loads, what it reads, and what it sends.

<script async src="https://<site>.<our domain>/embed.js"></script>

The script is built from the service's repository, served from the site's own address on the service, and runs on the publisher's Ghost site: the only origin from which Ghost's members endpoints answer with the visitor's session.

What it does, in order

  1. GET https://<site>.<our domain>/api/embed/config (cached five minutes, answered with Access-Control-Allow-Origin for the site's origin): the mapped tier ids, the button label, the landing page's address, and the site's members API address (<ghost url>/members/api, subdirectory aware).
  2. GET <members api>/member with the site's own cookie: who the visitor is. A visitor who is not signed in, or holds no mapped tier, gets no button; any data-enova-contribute element becomes a link to the landing page.
  3. On click: a blank tab is opened at once (popup blockers allow that only inside the click), then GET <members api>/session returns the member's identity token (a JWT valid ten minutes), and a form in that tab posts token top-level to POST https://<site>.<our domain>/auth/member. The token never enters a URL.
  4. The service verifies the token against the site's JWKS (/members/.well-known/jwks.json, RS512, issuer and audience <site>/members/api, scope members:identity), accepts it once, maps the member through the Admin API, checks the tier, and answers a redirect to the dashboard with a cookie bound to that site.

What it never does

It sets no cookies on the site, reads no cookie, writes no HTML from data it received, and talks to nobody but the site and the service's config endpoint. It works with a content-security policy on the site that allows the script's origin; it needs no unsafe-inline.

Ghost(Pro)

Hosted sites may sit behind an edge challenge on the members endpoints; the script runs as a top-level page script so a challenge can be served. If the members endpoint cannot be read, the email sign-in on the landing page is the fallback.

On this page