Merge pull request 'fix(inbound): refresh socket.io auth on (re)connect via callback' (#9) from fix/socket-auth-callback-refresh into main

This commit was merged in pull request #9.
This commit is contained in:
h z
2026-05-26 12:51:04 +00:00
2 changed files with 36 additions and 2 deletions

View File

@@ -261,9 +261,26 @@ export class FabricInbound {
const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token;
if (!tok) if (!tok)
continue; continue;
// Use the *callback* form of `auth` so socket.io re-evaluates the JWT
// on every (re)connect. The single-shot `auth: { token: tok }` shape
// captured the token in closure: after socket.io's silent auto-reconnect
// the backend got the same JWT that expired ~15 min into the session
// (guildAccessToken TTL = 900s) and silently rejected the handshake at
// the application layer. The client's `connect` event still fired (TCP
// succeeded), so the plugin happily ran the channel-resync, emitted
// `join_channel` into the void, and logged "joined N channel(s)" while
// the backend was actually broadcasting message.created to a room with
// zero subscribers. End user symptom: DMs to agents silently dropped.
const socket = io(`${g.endpoint}/realtime`, { const socket = io(`${g.endpoint}/realtime`, {
transports: ['websocket'], transports: ['websocket'],
auth: { token: tok }, auth: (cb) => {
// Best-effort fresh token; on transient failure fall back to the
// last known good one. tokenCache also keeps HTTP calls (attachment
// download / reply post) from 401'ing in the same window.
this.freshGuildToken(agentId, g.nodeId, session)
.then((fresh) => cb({ token: fresh ?? tok }))
.catch(() => cb({ token: tok }));
},
autoConnect: false, autoConnect: false,
}); });
// Tracked socket.io rooms for this (agent, guild). The initial fetch // Tracked socket.io rooms for this (agent, guild). The initial fetch

View File

@@ -325,9 +325,26 @@ export class FabricInbound {
for (const g of session.guilds) { for (const g of session.guilds) {
const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token;
if (!tok) continue; if (!tok) continue;
// Use the *callback* form of `auth` so socket.io re-evaluates the JWT
// on every (re)connect. The single-shot `auth: { token: tok }` shape
// captured the token in closure: after socket.io's silent auto-reconnect
// the backend got the same JWT that expired ~15 min into the session
// (guildAccessToken TTL = 900s) and silently rejected the handshake at
// the application layer. The client's `connect` event still fired (TCP
// succeeded), so the plugin happily ran the channel-resync, emitted
// `join_channel` into the void, and logged "joined N channel(s)" while
// the backend was actually broadcasting message.created to a room with
// zero subscribers. End user symptom: DMs to agents silently dropped.
const socket = io(`${g.endpoint}/realtime`, { const socket = io(`${g.endpoint}/realtime`, {
transports: ['websocket'], transports: ['websocket'],
auth: { token: tok }, auth: (cb) => {
// Best-effort fresh token; on transient failure fall back to the
// last known good one. tokenCache also keeps HTTP calls (attachment
// download / reply post) from 401'ing in the same window.
this.freshGuildToken(agentId, g.nodeId, session)
.then((fresh) => cb({ token: fresh ?? tok }))
.catch(() => cb({ token: tok }));
},
autoConnect: false, autoConnect: false,
}); });
// Tracked socket.io rooms for this (agent, guild). The initial fetch // Tracked socket.io rooms for this (agent, guild). The initial fetch