Skip to content

The person behind a page

Markdown

A page, and the server behind it, both need to know which person they serve.

Who is behind a request: x-apigami-user-ref

Section titled “Who is behind a request: x-apigami-user-ref”

Every route at your app’s own address knows the person in front of the browser, as req.user would in another framework: a page, a route of your own API, /oauth/callback, a file. Apigami keeps their session in a cookie of your address and hands your code their user_ref in a header, the same user_ref your actions read from GET /v1/me:

export default {
fetch(request: Request) {
// Null for a visitor, in the side panel, and on any request another site sent.
const userRef = request.headers.get('x-apigami-user-ref')
return new Response(userRef ? 'Hello again' : 'Hello, visitor')
},
}
  • The session starts once. The first time a page calls apigami.ready(), or the visitor apigami.connect(), the browser goes through /__apigami/start on your address, then Apigami’s connect page, and comes back with the session: without a click when the person already approved your application, after « Continue » otherwise. It lasts until 30 days go by without a visit, and ends sooner when the person disconnects from your application, signs out of Apigami’s dashboard, or their account is disabled.
  • It counts for your own pages’ requests and for a top-level navigation by GET or HEAD: a bookmark, a link, a service sending the person back. Never for a request another site or application sends: a form, a fetch, a WebSocket from another origin, or a page of yours in someone else’s frame. A link from anywhere still reaches you with the person, so keep every GET free of side effects.
  • Nobody forges it. Every x-apigami-* header and X-Forwarded-Host a request carries is taken out before your code, and Apigami’s cookies are taken out of Cookie.
  • It is absent in the side panel, framed by the dashboard, whose page gets its token by message; on a page of your own site, which Apigami does not serve; for a visitor; and when Apigami’s API does not answer within two seconds, served as a visitor, the session kept.
  • An action Apigami calls carries X-Apigami-Run, never x-apigami-user-ref. Actions, triggers and the calls of another application reach your code from Apigami’s servers, with no cookie: read the person with GET /v1/me under that token.
  • /__apigami/ is Apigami’s. Those paths never reach your code, and a version holding a file under __apigami/ is refused (reserved_envelope_path). Your entrypoint exports default { fetch(request, env, ctx) }: the validation warns about a missing one, or a node:http or Express server (no_default_fetch), and refuses Deno.serve (deno_api_refused).

Your neighbours under apigami.tech. Until browsers treat each application there as a site of its own, the other applications count as the same site as yours. Apigami ignores your session for their forms, fetch, WebSockets and frames, and three risks remain, accepted for now:

  • another application can show a page of yours in a frame, where your page’s own requests carry the person, and trick them into a click. Send Content-Security-Policy: frame-ancestors 'self' https://app.apigami.com chrome-extension: to refuse it, which still lets the side panel show your pages;
  • a link from another application reaches you with the person, as on any site;
  • another application’s page can set hundreds of cookies on apigami.tech from its script, pushing your session out of that browser, or making your address unusable there.

A domain of your own is a site of its own, and none of them applies there.

How a page knows who is viewing and calls its actions

Section titled “How a page knows who is viewing and calls its actions”

A page is shown in one of two ways, and apigami.js handles both:

  • Full page, in a tab of its own. At your app’s own address, apigami.js asks the person’s session for their token, POST /__apigami/token on your address, and renews it there; nothing is ever in the page’s address. A browser without a session there is a visitor’s (A visitor on your page): apigami.session() says whether a person is connected, and apigami.connect({ next }) gives the visitor a session and brings them back to next.
  • In the side panel, a side_panel page, or your settings page as the panel’s last tab, in a frame whose parent is the Apigami dashboard, https://app.apigami.com. The panel sends the page its token by message once it is shown, and apigami.connect() does nothing there.

In the side panel, the request that loads the page identifies neither the user nor the browser. Full page, your server reads the person in x-apigami-user-ref, never the browser. Load apigami.js and let it find out:

<script src="https://api.eu.apigami.com/apigami.js"></script>
<script type="module">
const session = await apigami.session()
if (!session.connected) {
// A visitor, full page: back here with a session once connected.
document.querySelector('#sign-in').onclick = () => apigami.connect({ next: location.pathname })
} else {
render(session.user_ref, apigami.state.url)
const cart = await apigami.call('getCart', {})
apigami.onChange((state) => render(session.user_ref, state.url))
}
</script>

A page that is only for connected people calls await apigami.ready() instead, which connects a visitor first. The second script is type="module": an await outside a function is a SyntaxError in a plain <script>, and the page stays blank. Without modules, write apigami.ready().then(({ user_ref }) => …). Load it by this absolute address, never a relative one (Load apigami.js).

Member What it does
apigami.ready() A promise of { url, install_ref, user_ref, app_id, page_id, page_kind }, once the page has its token, from the person’s session full page or from the side panel, and GET /v1/me has said whom it serves. url is the address of the tab beside the side panel, null full page; user_ref is the person, the one to key their data on. If GET /v1/me refuses the token, as once the session ended, it rejects with an apigami.ApigamiError carrying that status; in the side panel, the next token tries again. Full page, it waits for a person: without a usable token, it first sends the visitor through apigami.connect(), which brings them back to this page with a session. Call it only where the page needs to know who is viewing.
apigami.session() A promise that sends nobody anywhere: { connected: true, … } with what apigami.ready() gives, once GET /v1/me has accepted the token, else { connected: false, app_id } for a visitor, with no token or with one GET /v1/me refused. In the side panel, it waits for the first token.
apigami.connect({ next }) Full page: leaves for /__apigami/start on your address, then Apigami’s connect page, which gives the visitor a session, without a click when they already approved your application, and brings them back to next, a path of this page’s address, the current one by default. Work not saved by an action is lost. In the side panel it does nothing.
apigami.state The latest { url, install_ref, settings_needed }; in the side panel, null before the first message. Never the token. install_ref is the browser your actions drive, as your group knows it: the one beside the side panel, or, opened full page, the one the user just connected, once apigami.ready() resolves; null when the page drives none. settings_needed is what GET /v1/me lists (Ask a person to set up or reconnect), [] until then.
apigami.onChange(fn) Calls fn(state) at each later change of url or install_ref: added after await apigami.ready(), it is not called for the state the page already has, which apigami.state holds. Not when the token is renewed: apigami.js keeps the latest one for you. Returns the function that removes fn.
apigami.call(action, input, { app }) POST https://api.eu.apigami.com/v1/apps/{app_id}/actions/{action} with the current token and input as JSON. With app, the app_id of an application yours calls, a brick’s settings actions for instance, it calls that application’s action instead, for the same person, as your dependencies allow. Full page without a usable token it calls without one: only an action your manifest lists in open_actions answers, any other answers 401 bad client credentials and apigami.js then sends the visitor through apigami.connect(); your own action’s 401 does not. Resolves to the JSON body, or the text. Any other status than 2xx rejects with an apigami.ApigamiError carrying status, body and run_id, and so does a token GET /v1/me refused. A 202 { run_id, state: 'running' } is an action still running after thirty seconds, and a page cannot read the rest: GET /v1/runs/{run_id} takes a member’s credential, not a page’s token. Keep an action a page calls under thirty seconds, or have it store its result where a second action reads it.
apigami.me() GET /v1/me with the current token.
apigami.open(appId, path) On a click: opens a window on path at the address of an application yours calls, where it gets its own session for this person, without a click since they approved yours (Show the settings of the bricks you call). Call it in the click handler itself: it opens the window before it awaits anything. Resolves when the person is back, on that window’s { type: 'apigami:settings-changed' } message or when this window gets the focus again. Rejects when the browser blocked the window, for a visitor, and when your application may not call that one.

apigami.js accepts a message only from the dashboard’s origin and from the window above the page. It keeps the token in memory, never in localStorage, sessionStorage or a cookie of its own. Full page at your app’s address, a reload asks the session for it again; on your own site, it keeps it in the tab’s history entry, which only that tab can read. The one thing it writes to sessionStorage is when it last sent that page to be connected. It never navigates the frame. In the side panel and on your own site, it sends the page’s uncaught errors to app_logs with source: 'pages', ten a minute at most, including up to ten raised before the first token arrived. In a frame other than the side panel, where nothing sends it a token, it says so in the console after five seconds; a page in a tab of its own is a full page, open to visitors.

The call needs no key, no user_ref and no install_ref: the token already names the user and the browser, and naming them again is a 400. It runs the version serving this page to this person at the moment of the call: production’s when production declares the page, else, for a member of your group, the latest version declaring it, which is how a --staging page reaches the actions it ships with. A page left open across a publication calls the new version: reload it, or keep the old action names for a while. From a page, only the status, the body and X-Apigami-Run-Id of the answer are readable: list any other header in your own Access-Control-Expose-Headers, together with X-Apigami-Run-Id. A token is accepted only from the origin of its page: the same token sent from another site is a 401.

Apigami adds no frame-ancestors to your pages, so any site can frame them. A frame from another site never carries the person, one from another application under apigami.tech does (Your neighbours). To keep them out, send Content-Security-Policy: frame-ancestors 'self' https://app.apigami.com chrome-extension:, which still lets the side panel show them.

The messages apigami.js exchanges, for a page that cannot load it

The dashboard sends the page, once it is shown, and again whenever the tab’s address or the token changes:

{ type: 'STATE_CHANGE', state: { url, install_ref, token } }
  • url is the address of the tab beside which the side panel is open.
  • install_ref is this browser as your group knows it, the same value your key reads elsewhere.
  • token is a run token of your application for this browser. In the side panel it lives five minutes and the panel renews it at half-life (A page token’s lifetime). Each renewal arrives as a new STATE_CHANGE and ends the previous run, so always use the latest token. Both install_ref and token are null when no run could be opened, and the side panel then shows why above your page.

Answer each message with STATE_CHANGE_ACKNOWLEDGE carrying the state you received; until you do, the same state is sent again every 250 ms. Install the listener first, then announce the page with APIGAMI_PANEL_READY. Check the sender’s origin and window on every message. Never navigate the frame in response to STATE_CHANGE: each load starts the exchange again, and a page that reloads itself in a loop is suspended by the side panel. Call an action with the token in X-Apigami-Run or Authorization: Bearer.

<!doctype html>
<meta charset="utf-8">
<button id="search" disabled>Search</button>
<pre id="out"></pre>
<script>
const DASHBOARD = 'https://app.apigami.com';
const ACTION = 'https://api.eu.apigami.com/v1/apps/YOUR_APP_ID/actions/searchProducts';
let latest = null; // { url, install_ref, token }, in memory only
window.addEventListener('message', (event) => {
if (event.origin !== DASHBOARD || event.source !== window.parent) return;
if (event.data?.type !== 'STATE_CHANGE') return;
const state = event.data.state;
if (typeof state?.url !== 'string') return;
latest = state; // a renewed token replaces the old one, which no longer works
document.getElementById('search').disabled = !state.token;
window.parent.postMessage({ type: 'STATE_CHANGE_ACKNOWLEDGE', state }, DASHBOARD);
});
window.parent.postMessage({ type: 'APIGAMI_PANEL_READY' }, DASHBOARD);
document.getElementById('search').addEventListener('click', async () => {
const response = await fetch(ACTION, {
method: 'POST',
headers: { 'X-Apigami-Run': latest.token, 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'coffee' }),
});
document.getElementById('out').textContent = `${response.status} ${await response.text()}`;
});
</script>

The person opens the side panel by clicking the Apigami icon in Chrome’s toolbar, under the puzzle piece until they pin it, on a tab where a side_panel page of an application they use matches; otherwise that click opens Apigami’s settings. When several pages match, the panel shows one tab per page, named by its title, your settings page last.

Your page knows that tab by its address only: apigami.state.url, and apigami.onChange when it changes. It gets no tab id, and neither does an action: install_ref names the browser, not the tab. To read that tab, an action takes the url in its input, calls tabs.list on its domain, and passes the id of the tab with that url, the active one if several have it, as tabId to tabs.read, tabs.parse or a gesture. Without a tabId, an operation acts on the first open tab of the domain, which need not be that one.

Load apigami.js by its absolute address, never a relative path

Section titled “Load apigami.js by its absolute address, never a relative path”
<script src="https://api.eu.apigami.com/apigami.js"></script>

The same address for every application and every page, whether Apigami or your own site serves it: a relative path reaches your own code, which does not serve the script. At your application’s own address, a page loading /apigami.js, or the older /__apigami/panel.js, never gets window.apigami: the validation refuses it (page_script_on_app_host) and so does publication (page_failed). Any other relative path is warned about (page_script_relative). A Content-Security-Policy needs https://api.eu.apigami.com in script-src, for the script, and in connect-src, for the calls it makes.

A visitor on your page: apigami.session() and apigami.connect()

Section titled “A visitor on your page: apigami.session() and apigami.connect()”

A page opened full page without a session is a visitor’s: the person’s first visit in this browser, a session that ended, an address someone shared. The page loads and shows what it shows anyone, and apigami.js navigates nowhere on its own. apigami.session() answers { connected: false, app_id } and sends nobody anywhere; apigami.call() reaches only the actions your manifest lists in open_actions. apigami.ready(), apigami.connect({ next }) and an action Apigami refuses for want of a person send the visitor to /__apigami/start?next=… on your own address, then to Apigami’s connect page, which brings them back to next, the current path by default, with their session: at once when they already approved your application, after « Continue », which connects them, otherwise. Your app’s bare address is therefore an install link too. An open action’s own 401, a wrong password, sends nobody. In the side panel, apigami.session() waits for the panel’s first token, and apigami.connect() does nothing.

At your application’s own address, apigami.js learns which application it is from the page’s address (GET /v1/pages/app). On a page of your own site, the connect link opens it with apigami_run and apigami_app in the query string, and apigami.connect() goes through /connect?app_id=…&next=…: keep every apigami_ parameter when your page rewrites its own address. If the page comes back without a usable token within a minute, apigami.js shows a short message with the link instead of going round again. The errors of a visitor’s page never reach app_logs, which needs a token.

A page token’s lifetime: 5 or 15 minutes, renewed

Section titled “A page token’s lifetime: 5 or 15 minutes, renewed”

A page holds a run token of your application for the person viewing it. apigami.js keeps it and renews it for you, so a page left open keeps working: none of these figures is a time after which it stops.

Where the page is The token lives Who renews it, and until when
Side panel 5 minutes The side panel, at half-life, for as long as it shows the page. Each new token ends the previous one.
Full page, at your app’s address 15 minutes The person’s session, at half-life, on the next request their browser sends to your address, apigami.js asking at half-life, when the tab comes back into view and when the network comes back. The session lasts until 30 days go by with no renewal. The previous token lives out its 15 minutes, for a call already sent with it.
Full page, on your own site 15 minutes apigami.js, at half-life, and when the tab comes back into view or the network comes back, for 12 hours after the person went through the link. Each tab renews its own.

When the session ends, or those 12 hours are over, nothing reloads: the page stays as it is, and the next apigami.ready(), or the next action Apigami refuses for want of a person, sends them to be connected again, which brings them back to the same path: without a click at your app’s address, while they are still connected to your application. An action your page calls gets a run token of its own, not the page’s.