feat(service): P2.3 Socket.io /message gateway + real SessionVerifier

open_thread (create-or-join) / send_message / read / delivered / typing; HS256
token verified on connect (SessionVerifier reuses AppTokenVerifier pattern);
OutboxBus bridge propagates REST/ingest messages to rooms (dedup vs inline emit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 01:12:49 +05:30
parent 827fb52f5f
commit 50ef34dd90
6 changed files with 375 additions and 5 deletions
@@ -0,0 +1,46 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import jwt from 'jsonwebtoken';
import type { MessagePrincipal } from '../messaging/message.service';
/**
* The real `session` port for P2: verifies a host app's HS256 token (reuses the
* support-service AppTokenVerifier pattern). Per-app secrets come from the
* APP_SECRETS env (JSON map keyed by appId). Expected claims: { sub, name?,
* appId, orgId?, tenantId? }.
*/
@Injectable()
export class SessionVerifier {
private readonly secrets: Record<string, string>;
constructor() {
try {
this.secrets = JSON.parse(process.env.APP_SECRETS ?? '{}');
} catch {
this.secrets = {};
}
}
verify(token: string): MessagePrincipal {
const decoded = jwt.decode(token) as jwt.JwtPayload | null;
const appId = decoded?.appId ? String(decoded.appId) : undefined;
if (!appId) throw new UnauthorizedException('missing appId claim');
const secret = this.secrets[appId];
if (!secret) throw new UnauthorizedException(`unknown app: ${appId}`);
let payload: jwt.JwtPayload;
try {
payload = jwt.verify(token, secret, { algorithms: ['HS256'] }) as jwt.JwtPayload;
} catch {
throw new UnauthorizedException('invalid app token');
}
if (!payload.sub) throw new UnauthorizedException('missing sub claim');
return {
userId: String(payload.sub),
appId,
orgId: payload.orgId ? String(payload.orgId) : `org_${appId}`,
tenantId: payload.tenantId ? String(payload.tenantId) : undefined,
displayName: payload.name ? String(payload.name) : undefined,
};
}
}