> ## Documentation Index
> Fetch the complete documentation index at: https://obversa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Surface

> @obversa/surface: one loopback server, one session for one decision, one framed result.

`@obversa/surface` runs a surface: a small page for one decision, served
from one loopback server for one session, returning one result. It owns no
callback state, no routing and no run records. To learn what a surface is
and run one, read [Surfaces](/docs/concepts/surfaces); this page is the exact
rules a host or a page must keep to work with one.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @obversa/surface
  ```

  ```bash pnpm theme={null}
  pnpm add @obversa/surface
  ```
</CodeGroup>

Included in `@obversa/obversa`.

## Quickstart

`runSurface` is the one call an app makes: it starts the session, places
the page, waits for the decision, frames it on stdout and shuts down. The
result comes back between app-named frame lines, so a caller finds it in
mixed output:

```text Framed result theme={null}
<<<REVIEW_RESULT_V1>>>
{"schemaVersion":1,"app":"review","status":"completed","operationId":"…","payload":{…}}
<<<END_REVIEW_RESULT_V1>>>
```

`status` says how the session ended: `completed`, `cancelled`, `timed_out`
or `interrupted`. `payload` is the completed value after redaction, and
`operationId` the id the browser acknowledged. `parseFramedResult` pulls
one framed result out of captured stdout, and `frameResult` writes one.
`obversa-review` from [Surface Diff](/docs/packages/surface-diff) is a surface
built this way.

## Start a session

`startSurface` binds an ephemeral port on 127.0.0.1. The page URL carries a
session token in its fragment; every API request must send it as a bearer
token, checked in constant time. A request with a wrong `Host` is refused,
and so is a state-changing request with a wrong `Origin`. JSON bodies are
bounded and responses carry strict security headers. A session ends exactly
once: the app completes it with a payload, the user cancels, the lease or
session times out, or the caller interrupts. The browser acknowledges the
decision, a timeout covers a browser that never does, and
`waitForDecision()` gives the caller the one result.

## Complete with a payload

Payloads pass secret redaction unless the app completes with an explicit
verbatim opt-in, for content that must not be rewritten, such as review
annotations that quote code. A payload is its observable own data: the
values of its own enumerable string-keyed properties and array items,
recursively, as one JSON serialisation reads them. That is exactly what the
caller receives. A payload JSON can't carry whole is refused and the
session stays open: a cycle, a BigInt, a function, a symbol, `undefined`, a
non-finite number, negative zero, a non-enumerable or symbol-keyed
property, a hole or extra property in an array, a Proxy, or an object whose
prototype is neither Object's nor none. One exception: a value whose
`toJSON` is callable (a Date, for instance) is replaced by what `toJSON`
returns before anything else looks at it, and that return must pass the
same rule.

When two authenticated requests race to complete, exactly one succeeds and
the other gets a conflict. A completion is serialised once, when it's
claimed, and the caller receives that plain snapshot. Once a handler has
completed the session, its response is the fixed acknowledgement (`ok` and
the operation id), sent the moment the completion is claimed, so a
connected browser holds its operation id before the caller learns of the
completion. The acknowledgement timeout starts when that answer has gone
out, or at the disconnect if the connection closed first; when it elapses,
the caller learns of a completion the browser never received. The session
never waits without a bound.

`terminalPayload(status)` lets a session that ends without the app's
completion still carry an outcome payload the caller can route. It passes
redaction unless `terminalPayloadVerbatim` is set. Without the hook, those
statuses carry a null payload.

## Hand over files

Content that must move as files travels through a private temporary
directory (0700, files 0600) with SHA-256 hashes in the manifest, so the
consumer verifies the bytes before use: `createPrivateTransfer` makes one
and `removeTransfer` cleans it up. Diagnostics stay on stderr.

## Place the page

`openSurfaceUrl` opens the page in the selected host's native pane, trying
three places in order:

1. **The host's placement command**, the value of `OBVERSA_SURFACE_BIN`,
   and only when it's an absolute path. A relative or bare value is
   reported on stderr and skipped, because a bare name looked up on `PATH`
   would hand the launch URL to whatever executable sat first on it.
2. **The platform browser, on macOS only**, by its system path
   (`/usr/bin/open`). Linux and Windows skip this step, because `xdg-open`
   runs its own helpers by bare name and `cmd /c start` would take the URL
   as shell text.
3. **Printing the URL**, the last resort everywhere.

The placement command receives a one-time launch URL: single use, valid for
a minute, answering with one redirect to the page. It never receives the
page URL with the token in it. A command still running five seconds after
launch is taken to have opened the page.

## Build the page

`@obversa/surface/client` exports `createSurfaceClient`, the no-framework
browser kit: it reads the token from the fragment, removes it from the
address bar, keeps the lease alive with a heartbeat, wraps every call with
the bearer token, and acknowledges the terminal decision on submit or
cancel.

## Options

### `sessionTimeoutMs`, `leaseTimeoutMs`, `ackTimeoutMs`

The three bounds on a session: how long it may live in all (default
14,400,000 ms, four hours), how long the browser may go without a heartbeat
before the lease lapses (300,000 ms), and how long the caller waits for the
browser's acknowledgement (30,000 ms).

### The rest

| Field | Type | Default | Description |
| - | - | - | - |
| `app` | `string` | required | The app name; it names the frame markers. A missing name throws `TypeError`. |
| `assets` | directory | none | Static files the page is served from. |
| `api` | handlers | `{}` | The session API the page calls. A handler that returns `{ verbatim: true }` sends its content byte-exact. |
| `surface` | identity | none | The `surface` field of the result: package and resolved version. |
| `terminalPayload` | `(status) => payload` | none | The payload for a session that ends without the app's completion. |
| `terminalPayloadVerbatim` | `boolean` | `false` | Skip redaction on that payload. |
| `open` | `boolean` | `true` | `runSurface` only: place the page with `openSurfaceUrl`. |
| `signal` | `AbortSignal` | none | `runSurface` only: interrupt the session when the caller cancels. |
| `stdout`, `ready` | stream, callback | `process.stdout`, none | `runSurface` only: where the frame goes, and a hook when the page is up. |

## Errors

* **`TypeError`** from `startSurface` when `app` is missing.
* **A refused completion** leaves the session open: a payload JSON can't
  carry whole, or a second completion racing the first (a conflict).

## API

* **`runSurface(options)`**: start, place, wait, frame, shut down.
  [Quickstart](#quickstart).
* **`startSurface(options)`**: the session alone. [Start a session](#start-a-session).
* **`frameResult`**, **`parseFramedResult`**, **`terminalResult`**,
  **`TERMINAL_STATUSES`**: the framed result. [Quickstart](#quickstart).
* **`createPrivateTransfer`**, **`removeTransfer`**: files to the caller.
  [Hand over files](#hand-over-files).
* **`openSurfaceUrl`**: placement. [Place the page](#place-the-page).
* **`redactText`**, **`sanitizeValue`**, **`safeText`**,
  **`assertExactKeys`**: the helpers the session applies to responses.
  **`httpError(message, statusCode)`**: an error carrying an HTTP status.
* **`@obversa/surface/client`**: `createSurfaceClient`. [Build the page](#build-the-page).

## Next steps

* [Surfaces](/docs/concepts/surfaces): what a surface is and why it ends once.
* [Review a diff in a host pane](/docs/hosts/review): a surface in use.
* [Obversa in cmux](/docs/hosts/cmux): the host placement command.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.