Skip to content

Authenticate conversation streams and retire the global feed - #192

Open
nirmalhk7 wants to merge 4 commits into
NTARI-RAND:mainfrom
nirmalhk7:nirmal/agrinet-authenticated-streams
Open

nirmalhk7 wants to merge 4 commits into
NTARI-RAND:mainfrom
nirmalhk7:nirmal/agrinet-authenticated-streams

Conversation

@nirmalhk7

@nirmalhk7 nirmalhk7 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What the change is

Protects live chat updates with account authentication and conversation ownership checks. Removes the unrestricted /events feed.

Why it is needed

The current stream opens before authentication. Someone who knows a conversation ID can subscribe to its private updates.

How the change works

The server checks the account token and owner before opening the stream. It closes streams when the token expires, the client disconnects, or the conversation is deleted locally.

The browser uses streaming fetch to send the token in an Authorization header. It stops the previous stream on account changes and stops retrying when authorization fails. Tokens stay out of URLs and browser storage.

This PR depends on #191 and currently includes its changes. Review #191 first and deploy both together. The host must supply a login token. Stream invalidation is local to one server process; this does not add cross-server revocation.

Screenshot of the actual UI after a live message arrives. Demo responses are synthetic; the stream request was checked for the account Authorization header.
Chat showing a live update received through the authenticated stream

Where it is involved

Agrinet's /stream/:conversationId endpoint, backend/lib/conversationStreams.js, conversation deletion, and frontend/chat-ui streaming client, chat window, and sidebar. Stream behavior and limits.

Verified locally: 37 Node tests, 73 Jest tests, 13 chat UI tests, the UI build, and DynamoDB ownership checks passed. Coverage includes access denial, expiry, fragmented events, account changes, and deletion races. Hosted workflows await maintainer approval.

Signed-off-by: Nirmal Khedkar <nirmalhk7@gmail.com>
Signed-off-by: Nirmal Khedkar <nirmalhk7@gmail.com>
Comment thread backend/server.js Outdated
req.on('close', () => sseClients.delete(res));
// Account-scoped streams share the same owner check as message reads/sends.
const conversationStreams = require('./lib/conversationStreams');
app.get('/events', chatAuth, (_req, res) => {
Comment thread backend/server.js Outdated
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
clients.forEach(res => res.write(payload));
}
app.get('/stream/:conversationId', chatAuth, asyncHandler(conversationStreams.open));
Comment thread backend/server.js Outdated
// Middleware
app.use(authMiddleware);
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/uploads', chatAuth, asyncHandler(require('./middleware/attachmentOwner')), express.static(path.join(__dirname, 'uploads'), {
Signed-off-by: Nirmal Khedkar <nirmalhk7@gmail.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, Nirmal. Locking /stream and /events behind an account token plus conversation ownership closes a real hole: on main, anyone with a conversation ID can read its live chat. Registering the stream before the ownership read, revoking even pending streams on delete, closing on token expiry, and using the same 404 for foreign, legacy and missing chats are all careful work. The tests are strong: they caught 31 of the 40 bugs we planted.

What I tested:

  • 5154bdf via merge ref cfc4f03, plus a fresh merge onto current main 7524c48. That merge has the same code as your new head 4571d3a; only the order of two CONFORMANCE.md sections differs.
  • Results: backend 37/37 node --test and 73/73 Jest, chat-ui 13/13, vite build, frontend lint, test:planner and next build all pass. The two new backend test files also passed 8 more reruns.

Requesting changes because of one merge blocker:

  1. The CodeQL gate fails with 5 high js/missing-rate-limiting alerts: server.js:67, :70 and :83, conversationRoutes.js:8 and communicationRoutes.js:8. Main's ruleset blocks merges on these. Two are real:
    • /uploads now scans the whole Messages table on every request (message.js:69-84).
    • /stream has no cap on open streams per user.
      Please add express-rate-limit after chatAuth, keyed by req.user.userId, at least on /uploads and /stream. Cap open streams per user in conversationStreams.open (429 above about 5, decremented in cleanup()), and drop chatAuth from the 410 /events route. The alerts that moved over from main (#5, #15, #16) can be rate-limited too or dismissed by a maintainer with a reason.

Should fix (fine as follow-ups):

  1. Attachment lookup: serve files at /conversations/:id/attachments/:name behind conversationOwner and Query that one partition, or add a filePath GSI.

  2. Sign-in state: nothing calls setAccountToken yet. Please show a visible "sign in" state on 401 in Sidebar.jsx:29-36 and ChatWindow instead of only logging.

Nits:

  1. Merge order: main only allows squash or rebase merges, so once #191 lands this branch will conflict in CONFORMANCE.md and conversation-ownership.md. Please rebase then so the PR holds only the stream commit. While you're there, rename "A1/A2" to #191/#192 in authenticated-streams.md:3, :44 and conversation-ownership.md:47-48.

  2. Flaky test: conversationStreams.test.js:29 can fail near a second boundary. Sign with exp = Math.ceil(Date.now()/1000) + 1 and raise the 1500 ms timeout above 2 s.

  3. Attachment names with %, # or ? can't be downloaded (accountClient.mjs:43-50). Validate the raw name and request '/uploads/' + encodeURIComponent(name).

  4. A frame over 1 MiB makes the client reconnect every second. Treat it as a terminal error instead.

  5. Older issues for separate tickets, not this PR:

    • Deleting a chat with more than 25 messages leaves them behind (message.js:43-63).
    • Each token and message is sent twice on the same stream (communication_controller.js:13-29).
    • The ownership script isn't run in CI yet.

I'll re-test once the rebased head is up.

Apply account request budgets and cap pending plus active streams at
five per account. Release capacity on expiry, disconnect and deletion,
and return 410 for the retired global feed without authentication.

Carry shared ownership and attachment fixes. Stop oversized-frame
retries while preserving valid frames coalesced in one network chunk.
Add regression coverage and update deployment documentation.
@nirmalhk7
nirmalhk7 force-pushed the nirmal/agrinet-authenticated-streams branch from 094bc9a to fc1c498 Compare October 31, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants