@urun-sh/core
v0.2.26
Published
Core transport and channel primitives for the urun TypeScript SDK
Maintainers
Readme
@urun-sh/core
Core TypeScript proxy for apps deployed on urun from Python.
Install
npm install @urun-sh/coreQuick start
import { App } from '@urun-sh/core'
const app = App('my-app', {
baseUrl: 'https://urun.sh',
orgId: 'your-org-id',
jwt: await getCurrentUserJwt(), // advanced/manual path; @urun-sh/react can resolve WorkOS tokens for you
authProvider: 'workos',
})
const session = app.generate({ prompt: 'A sunset' })
// Consume the stream the runtime produces (direction is decided by first use). Tracks
// arrive ASYNCHRONOUSLY — subscribe with `.on('track', …)`; never read `.track` once.
const video = session.stream('video')
video.on('track', (track) => {
if (track) videoElement.srcObject = new MediaStream([track])
})
// Drive the app by writing DESIRED state into the control doc; the runtime reactively
// reconciles it (cheap config via an inline live read, structural change via `doc.bind`).
session.doc('control').set({ desired: { prompt: { text: 'A forest' } } })The names mirror the Python side 1:1:
App('my-app')mirrorsurun.App('my-app')app.generate()mirrors@app.function def generate(...)session.stream('video')mirrorsctx.stream('video', codec=...)session.doc('control')mirrorsctx.doc('control')createStore()mirrors the runtimestore = app.store
Streams are symmetric (produce-or-consume by first use)
session.stream(name) is one symmetric primitive — the first producer decides the
direction and the peer consumes (full-duplex = two named streams). On the runtime,
ctx.stream("name") is the polymorphic I/O endpoint: the pipeline HEAD when it drives and the
TAIL when it sinks. attach(track) makes the client the producer of that name (the inbound HEAD
the runtime composes with ctx.stream(name) | …); calling stream(name) without attaching
consumes the runtime's output. A second producer on the same name fails loud with
StreamDirectionConflict — never a silent merge. The runtime ctx.stream(codec=…) sink owns
media encode; the client only attaches the inbound track to a media element. Tracks arrive
asynchronously — subscribe with .on('track', …).
The control doc is the reactive desired-state surface
The client does NOT call an imperative start/update/stop. It writes desired.* keys into a
control doc with session.doc(name).set(...). The runtime reads CHEAP config inline from the
live doc.desired.* props and uses doc.bind(construct, [live props]) (React useMemo) to
reconstruct a stage on STRUCTURAL change — the single reconcile lane, no field-mapping, no
second hand-poll (pipeline.bind / stage.bind(doc, mapping) are retired). Keep the
desired.<path> names the client writes consistent with what the runtime reads.
API
App(appId, options)— reference a deployed urun app.app.<function>(args?)— call a Python function and get a session handle.session.stream(name)— get a named symmetric stream (media or data); first use decides direction.session.stream(name).track— read the latest inbound track (consume side).session.stream(name).on('track', cb)— subscribe to inbound tracks as they arrive.session.stream(name).attach(track)— produce: send a local browser MIC/AUDIO track upstream.session.stream(name).attachVideo(track)— produce: send a local browser VIDEO track upstream (a later call swaps the source viareplaceTrack).detach()/detachVideo()stop producing.session.stream(name).emit(payload)/.messages()— the DATA lane: produce a payload on the named stream / consume it as an async iterable (subscribing is opt-in).session.stream(name).seek(seconds)— seek a persisted finite stream to seconds from start.session.stream(name).seek('live')— return a live stream to the live edge.session.doc(name).get()— read the merged document snapshot.session.doc(name).set(patch)— write desired state into a session document.session.doc(name).on('change', cb)— react to runtime-published document changes.session.request(payload)/.requestStream(payload)/.complete(payload)— correlated request/response riding a session doc (reconnect-safe);requestStreamyields incremental deltas.session.onPhase(cb)/session.whenLive()— the nine honest lifecycle phases and the "is media ready?" gate (rejects with a typedSessionFailedError).session.end()/session.disconnect()— terminal release vs. detach-and-stay-reconnectable.Session.attach(sessionId, options)— join a running session as a consume-only viewer.createStore(options)— content-addressed global store:get(key)/has(key)reads andon(name, cb)/emit(name, data)global streams. (App-scopedstore.cacheis a runtime concern; content-addressed blobs —store.model/store.path/store.repo— are not app-scoped.)
Authentication
Browser clients authenticate with an organization ID plus a user/session JWT. During urun setup, the organization is provisioned with an auth provider configuration, such as a WorkOS client ID mapped to a JWKS URL. The TypeScript client forwards orgId, jwt, and optional authProvider; urun validates the JWT server-side against the JWKS configured for that organization.
For React + WorkOS apps, prefer @urun-sh/react with authProvider="workos"; it reads the WorkOS AuthKit context and forwards refreshed access tokens for you. Use the lower-level jwt option only for advanced/non-React integrations. Do not pass server API keys or long-lived secrets to browser clients.
