@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
@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:
- 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 onPATHwould hand the launch URL to whatever executable sat first on it. - The platform browser, on macOS only, by its system path
(
/usr/bin/open). Linux and Windows skip this step, becausexdg-openruns its own helpers by bare name andcmd /c startwould take the URL as shell text. - Printing the URL, the last resort everywhere.
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
TypeErrorfromstartSurfacewhenappis 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
- Surfaces: what a surface is and why it ends once.
- Review a diff in a host pane: a surface in use.
- Obversa in cmux: the host placement command.