Skip to main content
@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; this page is the exact rules a host or a page must keep to work with one.

Install

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:
Framed result
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 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

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.
  • startSurface(options): the session alone. Start a session.
  • frameResult, parseFramedResult, terminalResult, TERMINAL_STATUSES: the framed result. Quickstart.
  • createPrivateTransfer, removeTransfer: files to the caller. Hand over files.
  • openSurfaceUrl: placement. 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.

Next steps