# The person behind a page

> How your server and your page know who is viewing — the request header, apigami.js, a visitor, and how long a page keeps its access.

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

## 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`:

```ts
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](/docs/app-domain/#your-own-address-a-custom-hostname) is a site of its own, and none of them applies there.

## 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](#a-visitor-on-your-page-apigamisession-and-apigamiconnect)):
  `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:

```html
<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`](#load-apigamijs-by-its-absolute-address-never-a-relative-path)).

| 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](/docs/app-sign-in/#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](/docs/app-dependencies/#dependencies-reuse-a-brick) 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](/docs/app-dependencies/#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](#who-is-behind-a-request-x-apigami-user-ref)). 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.

<details>
<summary>The messages apigami.js exchanges, for a page that cannot load it</summary>

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

```js
{ 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](#a-page-tokens-lifetime-5-or-15-minutes-renewed)).
  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`.

```html
<!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>
```

</details>

### The tab beside the side panel

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

```html
<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()`

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

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.