Skip to content

Releases: cirreum/Cirreum.RemoteConnections.WebSockets

Release v2.0.2

Choose a tag to compare

@hyspdrt hyspdrt released this 30 Aug 02:19

Full Changelog: v2.0.1...v2.0.2

Release v2.0.1

Release v2.0.1 Pre-release
Pre-release

Choose a tag to compare

@hyspdrt hyspdrt released this 30 Aug 02:04

Full Changelog: v2.0.0...v2.0.1

Release v2.0.0

Choose a tag to compare

@hyspdrt hyspdrt released this 25 Aug 23:46

Cirreum.RemoteConnections.WebSockets 2.0.0 — a credential source can see what it is supplying for

Why this release exists

The credential seam shipped in 1.0 took no parameters. A host registered one source, and every
outbound connection in the application got the same answer from it — nothing in the call
distinguished a socket to the application's own API from one to a partner service. Where the host
could not infer an audience it answered with its own defaults, which on WebAssembly are Microsoft
Graph scopes, so the credential a connection got out of the box was aimed at the wrong resource.

That was reported against the SignalR transport by the first application built on this track. The
same seam is shared, so the same fix lands here.

What's new

The source is told what it is supplying for.
IRemoteConnectionCredentialSource.GetCredentialAsync receives a RemoteConnectionTokenRequest —
the endpoint, the Scopes the connection's options declare, and the connection type — and returns
AuthorizationHeaderSettings? rather than a bare token string.

An application names the audience on the options and writes no source at all:

options.Scopes = ["api://contoso/access_as_user"];

A source may be registered keyed to a connection type, and is preferred over the unkeyed
registration for that connection — so a bridge holding one socket to a provider and another to its
own backend can give each its own mechanism.

Any scheme may be resolved per attempt. A ClientWebSocket is single-use, so every attempt
builds a fresh one and sets its headers after the credential resolves. An ApiKey or other
non-Bearer credential therefore refreshes across reconnects exactly as a token does.

This is where the two transports legitimately differ. The SignalR client copies its configured
headers when it builds the client for an attempt, before the credential callback runs, so only
Bearer can be resolved per attempt there. Here the ordering is the other way round, so nothing is
restricted. The difference is in the mechanisms, not in the design.

The behavioural change to read before deploying

A resolved credential now has three answers: a value to present, AuthorizationHeaderSettings.None
to connect deliberately without one, and null meaning none is available — which fails the
attempt
, naming the endpoint.

In 1.0 the last two were the same answer. A callback or source returning nothing produced an
unauthenticated upgrade that the server refused, which read as an application authentication bug.
Separating them is what turns a missing credential from a puzzle into a message.

If a connection is meant to be anonymous, say so with None.

Compatibility

Three mechanical changes — a namespace, a generic type argument on
WebSocketRemoteConnectionContext.Create, and the credential seam — plus the behavioural change
above. See MIGRATION-v2.md.

Applications registering through Cirreum.Runtime.RemoteConnections.WebSockets do not touch
Create directly, and feel this as the namespace change and the credential seam only.

The receive and reconnect loops, OnFrameReceivedAsync, the envelope, SubProtocol and send
serialization are untouched.

See also

  • Cirreum.Contracts 5.0.0 — the contracts, and the reasoning behind the credential shape.
  • Cirreum.RemoteConnections.SignalR 2.0.0 — the same seam on the other transport.

Release v1.0.1

Choose a tag to compare

@hyspdrt hyspdrt released this 25 Aug 15:00

Full Changelog: v1.0.0...v1.0.1

Release v1.0.0

Choose a tag to compare

@hyspdrt hyspdrt released this 24 Aug 10:20

Cirreum.RemoteConnections.WebSockets 1.0.0

First release of the raw WebSocket transport for Cirreum's caller-side connection abstraction.

What this is for

Raw WebSockets are the right transport when the wire format belongs to someone else — telephony
media streams, realtime speech APIs, agent host sockets — and when a service needs a long-lived
outbound channel of its own. SignalR is not an option there: those services do not speak it.

The platform supplies ClientWebSocket and nothing else. Everything a durable connection needs
around it — assembling messages, noticing a loss, reconnecting, re-presenting a credential,
reporting state — is left to each application, and that is where behaviour drifts.

This package supplies it, behind the same IRemoteConnection contract the SignalR transport
implements, so an application reads one connection abstraction whichever transport it uses.

What it provides

Derive from WebSocketRemoteConnection and expose the endpoint's messages as typed members.

Receive loop

Assembles multi-frame messages and hands each complete message to OnFrameReceivedAsync with
its type. A fault in that method is logged rather than taking the connection down, since once
overridden it is application code.

Frame routing

OnFrameReceivedAsync is the seam. Its default decodes Cirreum's { "method", "payload" }
envelope and dispatches to handlers registered through On<T> — the same envelope a Cirreum
server writes for a method-addressed push, so both ends interoperate without being configured
for it, and SendAsync<T> writes it on the way out.

Overriding the seam is how a connection bridges a protocol it does not own. The envelope and
the handler registry then play no part; frames arrive as bytes and the derived type owns the
discriminator. That is the case raw WebSockets exist for.

Reconnection

The transport has none, so the connection drives it: on an abnormal close it moves to
Reconnecting and retries indefinitely on a capped, jittered schedule, restoring Connected
when an attempt succeeds. OnReconnectedAsync runs first, which is where server-side state
that does not survive a reconnect gets restored.

Set Reconnect to false on the options and a loss ends at Disconnected instead, leaving
recovery to the application.

Credentials

A ClientWebSocket cannot be reconnected once closed, so every connect and reconnect attempt
builds a fresh one — and re-resolves the credential in the process. Refresh across reconnects
is therefore a consequence of the transport's own constraint rather than machinery added on
top.

Postures resolve in a fixed order: an explicit callback on the options, an explicit
authorization header, an explicit choice to connect without credentials, then an ambient
IRemoteConnectionTokenSource. With none of those available the connection fails rather than
connecting anonymously. Credentials travel verbatim, so a scheme prefix carried inside one
keeps routing dispatch.

Where a host cannot send headers on an upgrade — a browser — a bearer credential travels as an
access_token query parameter instead, which Cirreum.Services.Server 1.6.0 reads on a
connection endpoint. A non-bearer credential has no query equivalent and is rejected there
rather than being silently dropped.

Binary frames

SendBytesAsync writes a raw frame, for audio and for protocols this connection does not
encode. Sends are serialized, because a WebSocket permits one write at a time and concurrent
callers would otherwise corrupt the stream.

Subprotocol

SubProtocol reports what the server selected, read from the live socket rather than cached:
each reconnect negotiates afresh, and a connection that adapts its framing to the negotiated
protocol would otherwise carry a stale value across a reconnect. Offer protocols through the
transport configure delegate.

Not included

Request/response. The transport provides none, and synthesizing it would mean rebuilding the
correlation, timeouts and cancellation that a protocol with real invocation semantics already
has. A connection needing that shape wants the SignalR transport.

Requirements

  • Cirreum.Domain 4.3.1 or later, which carries RemoteConnectionBase and the connection
    lifecycle hooks.
  • Cirreum.Contracts 4.6.0 or later, which carries IRemoteConnection,
    RemoteConnectionOptions and IRemoteConnectionTokenSource. It flows in transitively.
  • No external transport package: ClientWebSocket ships in the framework.