# nucleic-edge

Cloudflare Worker for Nucleic's cloud edge. See `docs/CLOUD_INFRA.md` for the full design.

- **Phase A (now):** `POST /v1/heartbeat` — anonymous **DAI** (daily active installs) heartbeat →
  Workers Analytics Engine (`nucleic_dai`). Indexed by a random install UUID, so it counts
  installs, not users. `GET /health` for a liveness check.
- **Phase B/C (later):** WebSocket relay (Durable Object `Room`) + APNS approval push. The relay
  also writes a true daily-active-**user** metric (`nucleic_relay_dau`), indexed by `roomId` ≈
  account — see [Relay DAU](#relay-dau) below.

## Develop

```sh
npm install
npm run typecheck      # tsc --noEmit
npm test               # vitest — unit tests for the payload validator
npm run check          # wrangler deploy --dry-run (bundles, no upload, no auth)
npm run dev            # local server at http://localhost:8787
```

`POST` smoke test against a local `npm run dev`:

```sh
curl -i -XPOST http://localhost:8787/v1/heartbeat \
  -H 'content-type: application/json' \
  -d '{"installId":"11111111-2222-3333-4444-555555555555","platform":"macos","osVersion":"26.0","channel":"beta","appVersion":"0.1.0","build":"42","arch":"arm64","locale":"en"}'
# → 204
```

(AE writes are accepted but not queryable from local dev; verify counts after deploy.)

## Provision (one-time, Cloudflare dashboard / API)

1. Ensure the **`blakeslee.xyz` zone** is on this account (the `routes` custom domain needs it).
2. Deploy: `npm run deploy` — provisions `api.nucleic.blakeslee.xyz` + cert and creates the
   `nucleic_dai` (and, once the relay is live, `nucleic_relay_dau`) dataset on first write.
3. Query DAI — daily active **installs** (AE SQL API), e.g. last 30 days split by platform:

   ```sql
   SELECT toStartOfDay(timestamp) AS d, blob1 AS platform, count(DISTINCT index1) AS dai
   FROM nucleic_dai
   WHERE timestamp > now() - INTERVAL '30' DAY
   GROUP BY d, platform
   ORDER BY d
   ```

## Blob/index layout (keep in sync with the client + the SQL above)

`nucleic_dai` — indexed by the anonymous install UUID (counts installs, not users):

| Field | Slot |
|-------|------|
| `installId` | `index1` |
| `platform` | `blob1` |
| `osVersion` | `blob2` |
| `channel` | `blob3` |
| `appVersion` | `blob4` |
| `build` | `blob5` |
| `arch` | `blob6` |
| `locale` | `blob7` |

## Relay + APNS (Phase B/C)

The relay is **E2EE-opaque**: binary WebSocket frames are forwarded byte-for-byte and never
inspected (Noise runs end-to-end through it). A separate *text* control plane carries non-secret
relay metadata. See `docs/CLOUD_INFRA.md` §2–§3 and §2.1 for the token lifecycle.

### Relay DAU

Every admitted `/relay` connection writes one data point to the `nucleic_relay_dau` Analytics
Engine dataset, **indexed by `roomId`**. Because `roomId` is derived from the host's static public
key (`docs/CLOUD_INFRA.md` §2.1), one room ≈ one user/account — so `count(DISTINCT index1)` is a
true daily-active-**user** count, unlike `nucleic_dai` which counts installs. Same-day reconnects
collapse under the `DISTINCT`.

| Field | Slot | |
|-------|------|---|
| `roomId` | `index1` | user/account identity (host static-key hash) |
| `role` | `blob1` | `host` \| `client` |
| `deviceId` | `blob2` | for "distinct devices per user" slices |

```sql
SELECT toStartOfDay(timestamp) AS d, count(DISTINCT index1) AS dau
FROM nucleic_relay_dau
WHERE timestamp > now() - INTERVAL '30' DAY
GROUP BY d
ORDER BY d
```

### Endpoints

| Method & path | Auth | Purpose |
|---|---|---|
| `GET /relay?t=<connToken>` (Upgrade) | connection token | Join a room's WebSocket |
| `POST /v1/relay/token` | `RELAY_ADMIN_SECRET` | Host mints a 90-day membership token for a paired device |
| `POST /v1/relay/connect` | membership token (Bearer) | Trade membership → a ~2-min connection token |
| `POST /v1/relay/revoke` | `RELAY_ADMIN_SECRET` | Revoke a device on unpair (`{deviceId}`) |
| `POST /v1/push/notify` | `RELAY_ADMIN_SECRET` | Out-of-band APNS wake for `{deviceId}` |

Control-plane frames (text JSON over the room socket):
- phone → relay: `{"t":"register","pushToken":"<hex>","env":"sandbox|production"}`
- host → relay: `{"t":"wake","deviceId":"<id>"}` — pushes only if that device's socket is absent
- relay → all: `{"t":"presence","peers":[{deviceId,role}]}`

### Provision (one-time)

1. **KV:** `wrangler kv namespace create NUCLEIC_RELAY_TOKENS` and `... NUCLEIC_PUSH_TOKENS`; paste the ids into
   `wrangler.jsonc`.
2. **Relay secrets:** `wrangler secret put RELAY_TOKEN_SECRET` (random 32+ bytes), `RELAY_ADMIN_SECRET`.
3. **APNS:** create a `.p8` auth key (Apple Developer → Keys, Push enabled), then
   `wrangler secret put APNS_KEY_P8` (paste the PEM), `APNS_KEY_ID`, `APNS_TEAM_ID` (`L7UDTQ6F5W`),
   `APNS_TOPIC` (`xyz.blakeslee.nucleic.remote`), `APNS_ENV` (`sandbox` for TestFlight/dev, `production` for App Store).
4. Enable **Push Notifications** on the `xyz.blakeslee.nucleic.remote` App ID; ship the iOS app with `aps-environment`.
5. `npm run deploy` (creates the `Room` DO on first deploy).

### Smoke test (after deploy)

```sh
M=$(curl -s -XPOST https://api.nucleic.blakeslee.xyz/v1/relay/token \
      -H "authorization: Bearer $RELAY_ADMIN_SECRET" \
      -d '{"roomId":"r1","deviceId":"host1","role":"host"}' | jq -r .token)
C=$(curl -s -XPOST https://api.nucleic.blakeslee.xyz/v1/relay/connect \
      -H "authorization: Bearer $M" | jq -r .token)
websocat "wss://relay.nucleic.blakeslee.xyz/relay?t=$C"     # then a second client in room r1
```

> APNS delivery must be exercised from the **deployed** Worker — local `wrangler dev` can't do
> HTTP/2 to Apple ([workerd #4841](https://github.com/cloudflare/workerd/issues/4841)).
