# Server package

`crowne/server` sends events from a server, such as a Cloudflare Worker or Node, for actions that happen there, such as a payment, or for pages that run no script. It also serves the [first-party proxy](./proxy.md).

```sh
npm install crowne
```

It needs a key with the `events:write` scope. Make one on the org's **Keys** page in the app, and store it as a secret, such as `CROWNE_KEY`. Never send the key to a browser.

```js
import { env } from "cloudflare:workers";
import { crowne } from "crowne/server";

let analytics = crowne({ key: env.CROWNE_KEY, site: "YOUR_SITE_ID" });

export default {
  async fetch(request, env, ctx) {
    ctx.waitUntil(analytics.pageview(request));
    return new Response("Hello");
  },
};
```

## crowne(options)

| Option     | Use                                                                                  |
| ---------- | ------------------------------------------------------------------------------------ |
| `key`      | A key with the `events:write` scope, `crw_live_…`. Required.                         |
| `site`     | The id of the site the events count for. Required.                                   |
| `endpoint` | Where events go. Default: `https://in.crowne.io/v1/events`.                          |
| `fetch`    | The `fetch` function to send with. Default: the global `fetch`.                      |
| `ip`       | Reads the visitor's address from a request. See [Visitor address](#visitor-address). |

## pageview(request)

Counts the page that `request` asks for, as the visitor who sent it. It returns a promise; hand it to `waitUntil`, so the response does not wait for it.

## track(request, name, props?)

Counts a [custom event](./events.md) of the visitor behind `request`.

```js
ctx.waitUntil(
  analytics.track(request, "purchase", { plan: "pro", amount: 49 }),
);
```

`pageview` and `track` reject with the status and the reason when crowne refuses the event, such as for a bad key.

## proxy(request)

Answers the two paths of a [first-party proxy](./proxy.md):

- `GET` of a path that ends in `.js` answers crowne's script, cached for one hour.
- `POST` forwards the browser's events as it sent them, with the key and the visitor in the `Crowne-Visitor` header, and without cookies. It answers crowne's answer.
- Other methods answer `405`.

## Visitor address

crowne tells visitors apart by their address, so the package must know it, and the visitor must not be able to choose it.

On Cloudflare Workers, the package reads `CF-Connecting-IP`. Cloudflare sets this header, and a visitor cannot fake it. The package knows that it runs on Workers from `navigator.userAgent`, which needs the [`global_navigator` compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/). The flag is on by default from compatibility date `2022-03-21`. If your Worker uses an older date or the `no_global_navigator` flag, add `global_navigator` to `compatibility_flags`, or set the `ip` option.

Everywhere else, the package trusts no header, because a visitor can send any header, including `CF-Connecting-IP` and `X-Forwarded-For`. Set the `ip` option to read the address from a source you trust. Without an address, crowne still counts each page view and event, but as an anonymous visitor: it cannot tell apart visitors with the same browser, so it counts them as one. The package warns once when this happens.

| Where the server runs                                      | `ip`                                                         |
| ---------------------------------------------------------- | ------------------------------------------------------------ |
| Cloudflare Workers                                         | Not needed                                                   |
| Behind Cloudflare, with the origin open only to Cloudflare | `(request) => request.headers.get("cf-connecting-ip")`       |
| Vercel                                                     | `(request) => request.headers.get("x-vercel-forwarded-for")` |
| Fly.io                                                     | `(request) => request.headers.get("fly-client-ip")`          |
| Bun                                                        | `(request) => server.requestIP(request)?.address`, as below  |
| Deno                                                       | The handler's `info.remoteAddr.hostname`, as below           |
| Node                                                       | `req.socket.remoteAddress`, as below                         |

Read a host's own header only on that host. On any other host, the visitor can send it. Behind your own proxy, such as nginx, read the header that the proxy sets new on each request. Never use the first `X-Forwarded-For` entry, because the visitor controls it.

### Bun

`server.requestIP` gives the address of a request.

```js
import { crowne } from "crowne/server";

let analytics = crowne({
  key: process.env.CROWNE_KEY,
  site: "YOUR_SITE_ID",
  ip: (request) => server.requestIP(request)?.address,
});

let server = Bun.serve({
  fetch(request) {
    analytics.pageview(request).catch(console.error);
    return new Response("Hello");
  },
});
```

### Deno

Deno gives the address to the handler, not to the request. Make a client in the handler; it costs little.

```js
import { crowne } from "npm:crowne/server";

let options = { key: Deno.env.get("CROWNE_KEY"), site: "YOUR_SITE_ID" };

Deno.serve((request, info) => {
  let analytics = crowne({ ...options, ip: () => info.remoteAddr.hostname });
  analytics.pageview(request).catch(console.error);
  return new Response("Hello");
});
```

### Node

Node gives the address on the socket of each incoming message. It is the visitor's address only when no proxy is in front of the server. Behind a proxy, use the header that the proxy sets.

```js
import { createServer } from "node:http";
import { crowne } from "crowne/server";

let options = { key: process.env.CROWNE_KEY, site: "YOUR_SITE_ID" };

createServer((req, res) => {
  let request = new Request(`http://${req.headers.host}${req.url}`, {
    headers: req.headers,
  });
  let analytics = crowne({ ...options, ip: () => req.socket.remoteAddress });
  analytics.pageview(request).catch(console.error);
  res.end("Hello");
}).listen(3000);
```

## The visitor

Besides the address, the package reads the browser from `User-Agent`, the language from `Accept-Language`, and the place from Cloudflare's `request.cf`. Outside Cloudflare Workers, events have no place. To send one, use the [REST API](./api.md#post-v1events).
