Repository navigation
Service actors for the machine callers #33
Replies: 2 comments
|
Status check, 2026-09-22 (main at 23b983e) Nothing here is built: |
|
Closing as outdated. The premise has gone: the listener credential middleware and the Icecast listener hook were removed, the bridge routes are down to |
Uh oh!
There was an error while loading. Please reload this page.
As of: 2026-08-08, just after
listener.credential.middlewarelanded (896db1f).Three things inside the stack call the API as themselves rather than on behalf of an operator:
Liquidsoap (
POST /playout/bridge/aired), Icecast (POST /playout/bridge/listener), and the Spotify track shimin the other direction. All of them present the same static string, and none of them is an actor.
This file is the design for making them actors, and the reason it was not done when the question
came up.
What the tree does today
One secret, both directions.
StreamService.settings().playoutBridgeSecretis read once inPlayoutModule.readyand pushed intoLiquidsoapEndpoint.useSecret, and the same value is handedto
SpotifyShimClient.useSecretsand materialized intoradio.env.radio.liqchecks it on its owncontrol API;
PlayoutService.requireBridgeSecretchecks it, in constant time, on ours.The bridge routes are
operation(internal)indata/contracts/playout/playout.ck: they generate arouter but no SDK method, they carry no
policy, and they take the secret as a declaredPlayoutBridgeHeadersfield rather than through authentication. Icecast cannot set a header of itsown, so it sends the secret as HTTP basic and
apps/api/src/server/middleware/listener.credential.middleware.tsmoves it ontox-playout-secretbefore ServerKit's authentication middleware deletes
Authorization.Actorisuser | system | vendor(apps/api/src/modules/permissions/authorization.context.ts).Both bridge callers land in the unauthenticated branch of
authorizationContextMiddlewareand areclassified
{ kind: 'system', source: 'http' }, which is to say: not distinguished from each other,or from anything else that arrives without a bearer.
What the design is
A fourth actor kind,
service, backed by its own credential and its own permission tuples, so eachmachine caller authenticates as itself and is separately revocable.
@maroonedsoftware/authenticationalready ships the auth half:BasicAuthenticationHandlerdecodesthe credential and delegates to a
BasicAuthenticationIssueryou implement and register in thecontainer (
verify(username, password) -> AuthenticationSession). The username, ignored today, iswhat names the caller. So
icecast:<secret>andliquidsoap:<secret>stop being the samecredential presented twice and become two identities.
What has to exist around it:
time to seed
radio.envandicecast.xml.ServiceActorvariant on theActorunion, and a branch inauthorizationContextMiddlewarethat builds one. The
existsActivecheck there is written againstActorsRepositoryand humanlogins; a service actor needs the equivalent against its own store, not a person row.
policyon the bridge operations to replace thehand-rolled
requireBridgeSecret.memory (
LiquidsoapEndpoint,SpotifyShimClient) and Liquidsoap holds its copy fromradio.env,so a rotation is a settings write, a re-materialize and a reload of two processes, in an order
nothing has worked out yet.
Why it was not built when it came up
The credential is not per-caller yet. "Icecast as its own actor" reads as an auth-plumbing
change and is not one: while Liquidsoap and Icecast share a string, giving one of them an identity
gives the other the same identity for free. Splitting the secret comes first, and it touches
settings,
radio.env,icecast.xmland the shim.listener_addis a blocking call on the listener's own connection. Icecast holds the clientuntil
POST /playout/bridge/listeneranswers, which is whyPlayoutService.noteListenerdoes no databasework, takes no lock and returns a constant. Routing it through authentication as it stands adds, per
listener connect, an issuer verify, a session materialization, and then the
existsActivequery andplatform-role tuple read that
authorizationContextMiddlewareperforms. Two Postgres round trips onthe path that decides whether somebody hears the station. Any design here has to keep that path
free of I/O: verify against an in-memory credential seeded at boot, and resolve the actor without
touching the database, or leave the route where it is.
Scheme registration is global. Handling
basicmakes every route in the API accept basic, sothe secret stops being a credential for one gate and becomes a credential for the whole API,
narrowed only by tuples. That is the right shape once the tuples exist and each caller holds its
own secret. It is a strictly worse shape while one shared string would inherit whatever the union of
those permissions turns out to be.
Sessions are for logins.
AuthenticationSessioncarries an expiry, factors and a family id, andlives in Redis. Icecast presents its credential on every request and holds nothing between them, so
a service actor should mint nothing and store nothing: the session it produces is derived from the
credential each time, or the concept is being borrowed for something it does not fit.
When to build it
When there is a third machine caller, or the first time a bridge credential has to be rotated
without taking the other one down. Until then one shared secret checked in constant time on two
routes is honestly described by the code that does it, and the middleware above is the whole cost of
Icecast's inability to set a header.
If the render pipeline lands first (see
director-and-lineups), it arrives with its own machine caller and is
the natural moment: do the credential split, then the actor kind, then move the bridge routes onto a
policy, in that order. Each step leaves the station on air.
Related
apps/api/src/server/middleware/listener.credential.middleware.tsfor why the basic credential ismoved rather than handled, and the two alternatives that were rejected.
stream/README.mdfor the lease and the listener hooks, includingstream.listenerHooksfor anIcecast built without libcurl.
.claude/skills/contractkit/SKILL.mdonoperation(internal)and why the webhook policy shapedoes not fit these routes.
Moved from
docs/todo/service-actors.md. The file as it last stood: https://github.com/robert-dean/deadair/blob/71e431d45ce7b35a189c7d226832ec4a6247e9dc/docs/todo/service-actors.mdAll reactions