# @aphrodite/viewer

@aphrodite/viewer — part of the Oshun monorepo.

TypeScript package. Built with tsc/tsup; consumed by other workspaces.

## Scripts

| Command          | Definition             |
| ---------------- | ---------------------- |
| `pnpm build`     | tsup                   |
| `pnpm dev`       | tsx watch src/index.ts |
| `pnpm start`     | node dist/index.js     |
| `pnpm lint`      | eslint src/            |
| `pnpm typecheck` | tsc --noEmit           |

## Layout

- `src/`

## Protected live playback

`POST /api/streams/:streamId/playback` evaluates Aphrodite's authoritative
viewer policy, joins or idempotently refreshes a durable viewer lease, and
returns that lease plus a short-lived HLS/DASH Bearer grant with
`Cache-Control: no-store`. The strict request requires `idempotencyKey`,
`clientInstanceId`, `deviceType`, and `protocol`, and accepts an optional
bounded `lifetimeSeconds`. The server response supplies `sessionId`, the
rotating `leaseToken`, expiry, heartbeat cadence, and the authoritative presence
snapshot. Clients cannot select a tenant, publication, viewer, entitlement
decision, object prefix, or signing key. Age, sensual-tier, account/ban,
broadcaster-block, subscription/private-show, stream-state, and
ready-publication failures are explicit and fail closed. Ticket entitlement is
not configured yet and returns service unavailable rather than fabricated
access.

Send the lease token to
`POST /api/streams/:streamId/playback/:sessionId/heartbeat`; the heartbeat may
also carry one strict bounded QoE sample. End playback with the corresponding
`.../:sessionId/leave` route, and read exact active-session/unique-viewer counts
from `GET /api/streams/:streamId/presence`. The former
`POST /api/streams/:streamId/{join,leave}` routes return `410 Gone`; they no
longer disclose direct playback URLs or mutate counters.

Enable the runtime with the same signing/origin values used by the streaming
origin authorizer:

- `LIVE_MEDIA_PLAYBACK_ENABLED=1`
- `LIVE_MEDIA_PLAYBACK_KEY_ID`
- `LIVE_MEDIA_PLAYBACK_HMAC_SECRET` (at least 32 bytes)
- `LIVE_MEDIA_ORIGIN_BASE_URL` (HTTPS outside loopback tests)
- optional `LIVE_MEDIA_PLAYBACK_LIFETIME_SECONDS` and
  `LIVE_MEDIA_PLAYBACK_CLOCK_SKEW_SECONDS`
- optional `LIVE_MEDIA_VIEWER_LEASE_SECONDS` (15–300, default 45) and
  `LIVE_MEDIA_VIEWER_HEARTBEAT_SECONDS` (at least 5 and shorter than the lease)

Apply migration `00017_live_media_player_client` and enable the durable HLS/DASH
browser proof path with:

- `LIVE_MEDIA_PLAYER_CLIENT_ENABLED=1`
- `LIVE_MEDIA_PLAYER_CLIENT_WORKER_ID` (stable bounded deployment identity)
- optional `LIVE_MEDIA_PLAYER_CLIENT_LEASE_MS` (1,000–300,000; default 15,000)

When enabled, join returns a queued `clientOperation`; a heartbeat QoE sample
must include the exact `playbackGrantId`. The first persisted `playing` sample
with positive decoded frames changes that operation to `succeeded`, subsequent
heartbeats retain the original proof, and leave returns `released`. These
responses are `no-store`. The media Bearer is never accepted by the application
API or stored in the player ledger. Use `@oshun/live-media/browser` for the
corresponding HLS.js/dash.js MSE client and accessible protocol-neutral player
surface. Registration is restricted to browser sessions; native client operation
registration remains disabled.

Bearer authentication requires `VIEWER_JWT_SECRET` (or the legacy `JWT_SECRET`)
and verifies HS256 issuer/audience. Defaults are `aphrodite-auth`/`aphrodite`;
deployments may set `VIEWER_JWT_ISSUER` and `VIEWER_JWT_AUDIENCE`. The JWT `sub`
is the canonical user UUID and is mapped through `viewers.user_id`; it is never
treated as `viewers.id`.

## Working on this package

The fastest local loop is via Nx:

```bash
pnpm nx test aphrodite-viewer
pnpm nx lint aphrodite-viewer
pnpm nx build aphrodite-viewer
```

When Nx is unhappy (usually because another worktree has duplicate project
names), bypass it with the underlying tools from this directory:

```bash
npx tsc --noEmit
npx vitest run
```

See the [root README](../../../README.md) for monorepo-wide conventions (catalog
deps, commit format, git workflow). See the
[Contributor Guide](../../../docs/CONTRIBUTING.md) for the full PR workflow.
