Files
recflare/apps/notify
Crasho 26270e9c25 [studio] beta studio (#114)
* 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>
2026-10-05 14:39:22 -04:00
..
2026-10-05 14:39:22 -04:00
2026-06-09 00:49:11 -04:00
2026-09-14 13:05:08 -04:00
2026-09-18 17:55:55 -04:00
2026-06-09 00:49:11 -04:00
2026-06-09 00:49:11 -04:00
2026-06-30 23:57:29 -04:00

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 a connectionId / connectionToken and 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 get 426.
  • 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 its role claim.
  • 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/system MessageReceived to 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

  1. Client POST /hub/v1/negotiate, then opens a WebSocket to /hub/v1?id=<token>.
  2. Handshake: client sends {"protocol":"json","version":1}␞, server replies {}␞ (␞ = record separator 0x1e).
  3. Server immediately sends the OnConnect invocation on connect.
  4. 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.
  5. Server→client: Notification invocations carrying a JSON string { Id: <PushNotificationId>, Msg: { ... } }.
  6. 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.