* rrs stuff * studio service yay * rrs whitelist * moree studiooooooo fellowwww * rec room studio room loading yay * fixes yay * fix 2 because idk yayuh * Update cdn.app.ts * ccontent length fix? * Preserve content-length and upgrade metadata Return a real zero-filled byte array for CDN HEAD responses so the runtime preserves Content-Length while still sending no payload. In notify, re-create the Request with its original headers, set OWNER_HEADER from the validated token, and forward that Request to hub.fetch to avoid losing WebSocket upgrade metadata (which could trigger 500s in Durable Objects). These changes ensure the editor's size checks and WebSocket upgrades remain reliable. * fix x524234 * shirt thing? * [auth] remove grant table as we already have audit_log for now * rename table to generic device_grant * cleanup/lint --------- Co-authored-by: Devin Zuczek <devin.zuczek@gmail.com>
notify
Notifications Worker served on the notify subdomain. Hosts a SignalR hub at
/hub/v1.
The hub is implemented as a Durable Object (NotificationsHub) speaking the
SignalR JSON Hub Protocol over a hibernatable WebSocket. A single global DO
instance holds the shared hub state (one process across all connections).
Endpoints
POST /hub/v1/negotiate— SignalR negotiation. Returns aconnectionId/connectionTokenand advertises the WebSocket transport. The token is passed back as?id=on the WebSocket connect.GET /hub/v1(WebSocket upgrade) — the hub. Forwarded to the Durable Object. Non-upgrade requests get426.POST /internal/notify—{ playerId, notificationType, data? }. Send to a player's connections, queueing if they're offline. Admin-gated: requires a Bearer token carrying an admin role (developer/moderator) in itsroleclaim.POST /internal/broadcast—{ notificationType, data? }. Send to every connected client. Same admin-role gate as/internal/notify.POST /internal/coach-message-all—{ messageContent }. Broadcast a coach/systemMessageReceivedto every connected client (online-only; not persisted). Same admin-role gate.
Every /internal/* call — including one the role gate refuses, and one that 404s — is
recorded on the shared database's audit_log table, against the account that made it:
player_id is the caller, action is the path as a snake_case verb (coach_message),
and data carries the request body or query string plus the response status. The write
happens in the admin gate, so a new /internal/* endpoint is audited the day it is added.
A token carrying an admin role but no usable sub is refused (401) rather than let through
unattributable. A failed audit write never fails the call; it leaves an error in the worker
log. The table is owned by the api worker (apps/api/migrations/0024_audit_log.sql).
Hub protocol
- Client
POST /hub/v1/negotiate, then opens a WebSocket to/hub/v1?id=<token>. - Handshake: client sends
{"protocol":"json","version":1}␞, server replies{}␞(␞= record separator0x1e). - Server immediately sends the
OnConnectinvocation on connect. - Client→server invocations:
SubscribeToPlayers({ playerIds })— replaces this connection's subscriptions and flushes any queued notifications for those players.GetSubscriptions()→ completion with the subscribed player id array.
- Server→client:
Notificationinvocations carrying a JSON string{ Id: <PushNotificationId>, Msg: { ... } }. - Pings (type 6) are echoed.
State
Held in the DO's SQLite so it survives hibernation:
subscriptions(connectionId, playerId)— serves both the connection→players and player→connections lookups.pending(id, playerId, payload)— per-player queue delivered once the player subscribes.
A connection's rows are removed on webSocketClose.
TODO before production
- Authenticate the
/internal/*endpoints (or move them to a service binding / RPC reachable only by other workers). - Wire the real callers (relationships, gifts, storefront, events, chat, …) to
push through
notifyPlayer/broadcast. - Consider server-initiated keepalive pings if clients rely on them.