diff --git a/dist/fabric/src/inbound.js b/dist/fabric/src/inbound.js index f81d871..03bb9c1 100644 --- a/dist/fabric/src/inbound.js +++ b/dist/fabric/src/inbound.js @@ -261,9 +261,26 @@ export class FabricInbound { const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; 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`, { 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, }); // Tracked socket.io rooms for this (agent, guild). The initial fetch diff --git a/src/inbound.ts b/src/inbound.ts index 01e151b..1ce77f4 100644 --- a/src/inbound.ts +++ b/src/inbound.ts @@ -325,9 +325,26 @@ export class FabricInbound { for (const g of session.guilds) { const tok = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; 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`, { 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, }); // Tracked socket.io rooms for this (agent, guild). The initial fetch