Skip to content

feat(sse-module): server sent event - #2790

Open
Junjiequan wants to merge 25 commits into
masterfrom
server-sent-event
Open

feat(sse-module): server sent event#2790
Junjiequan wants to merge 25 commits into
masterfrom
server-sent-event

Conversation

@Junjiequan

@Junjiequan Junjiequan commented Jun 16, 2026

Copy link
Copy Markdown
Member

Description

Adds a Server-Sent Events (SSE) module that streams MongoDB change events to authenticated clients, so the frontend gets real-time dataset notifications without polling. Works across multiple backend instance because each instance tails MongoDB's change stream independently

⚠️ MongoDB must run as a replica set
This feature uses MongoDB change streams, which only work on a replica set — a standalone mongod has no oplog, and change streams read from the oplog. Without --replSet, the backend's change-stream listener throws and the feature is dead.

This is a deployment-breaking change for anyone running a standalone MongoDB. A standalone instance must be converted to a replica set (single-node is enough) in order to use server sent events.

Changes:

  • SSE module — service, controller, listener, and a collection registry. Exposes /events/stream (authenticated SSE) and /events/connections (active-connection count).
  • Change-stream listener — watches MongoDB for inserts on registered collections and converts them into SSE events.
  • Per-user connection limit — capped per user per application instance (MAX_CONNECTIONS_PER_USER_PER_INSTANCE).
  • Permission filtering — events are only sent to clients allowed to see the document, based on accessGroups.
  • JWT extraction — also accepts the token via a token query param, since EventSource can't set an Authorization header.
  • Docker Compose — MongoDB now runs as a single-node replica set instead of standalone

Tests included

  • Included for each change/fix?
  • Passing?

Documentation

  • swagger documentation updated (required for API changes)
  • official documentation updated

official documentation info

Summary by Sourcery

Introduce a server-sent events module to stream MongoDB change events to authorized clients and expose connection monitoring endpoints.

New Features:

  • Add a global SSE module with controller, service, and MongoDB change-stream listener to push dataset events to authenticated clients.
  • Expose an authenticated SSE stream endpoint for datasets and an endpoint to inspect active SSE connections.
  • Allow JWT authentication via either Authorization bearer header or token query parameter for compatibility with SSE connections.

Enhancements:

  • Limit concurrent SSE connections per user per instance and provide per-user connection counts for monitoring.
  • Configure MongoDB in E2E and local docker-compose setups as a replica set with a healthcheck that initializes the replica set for change-stream support.
  • Adjust mailer configuration typing to use MailerOptions["transport"] instead of a custom transport type.

Build:

  • Update E2E and local Docker Compose MongoDB services to run with replica set settings and a replica-set-aware healthcheck required for change streams.

@Junjiequan
Junjiequan marked this pull request as ready for review June 23, 2026 11:59
@Junjiequan
Junjiequan requested a review from a team as a code owner June 23, 2026 11:59
@Junjiequan
Junjiequan requested a review from Copilot June 23, 2026 12:00

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The JWT strategy now accepts tokens from a token query parameter for all routes; consider restricting this extractor to the SSE endpoints (or making it configurable) to avoid unintentionally broadening the attack surface for every authenticated endpoint.
  • In SseController.stream, SseService.getEvents already yields MessageEvent objects but you then wrap them again as { data: payload }; either return the observable directly or change SseService.emit to work with raw payloads so you don’t end up nesting MessageEvents.
  • In SseService, accessGroups is left without an explicit type or default and then accessed with optional chaining; it would be clearer and safer to type it as AccessGroupsType | undefined and/or initialize it to a sensible default (e.g. { admin: [] }) to avoid relying on undefined checks throughout the access logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The JWT strategy now accepts tokens from a `token` query parameter for all routes; consider restricting this extractor to the SSE endpoints (or making it configurable) to avoid unintentionally broadening the attack surface for every authenticated endpoint.
- In `SseController.stream`, `SseService.getEvents` already yields `MessageEvent` objects but you then wrap them again as `{ data: payload }`; either return the observable directly or change `SseService.emit` to work with raw payloads so you don’t end up nesting MessageEvents.
- In `SseService`, `accessGroups` is left without an explicit type or default and then accessed with optional chaining; it would be clearer and safer to type it as `AccessGroupsType | undefined` and/or initialize it to a sensible default (e.g. `{ admin: [] }`) to avoid relying on undefined checks throughout the access logic.

## Individual Comments

### Comment 1
<location path="src/serverSentEvent/sse.service.ts" line_range="21-24" />
<code_context>
+    string,
+    { user: JWTUser; subject: Subject<MessageEvent> }
+  >();
+  private accessGroups;
+
+  constructor(private configService: ConfigService) {
+    this.accessGroups =
+      this.configService.get<AccessGroupsType>("accessGroups");
+  }
</code_context>
<issue_to_address>
**suggestion:** Untyped `accessGroups` field can degrade type safety and hide config issues.

Because `accessGroups` is implicitly `any`, the `this.accessGroups?.admin?.includes(g)` call in `emit` is not type-checked, so issues like `admin` not being an array won’t be caught at compile time. Please type this explicitly, e.g. `private accessGroups?: AccessGroupsType;` (or a narrower type), and consider a safe default or fail-fast behavior when the config is missing or malformed.

Suggested implementation:

```typescript
  private accessGroups?: AccessGroupsType;

  constructor(private readonly configService: ConfigService) {
    const accessGroups = this.configService.get<AccessGroupsType>('accessGroups');

    if (!accessGroups) {
      // Fail fast if configuration is missing. If you prefer a softer behavior,
      // replace this with a default or logging-only behavior.
      throw new Error('SseService: "accessGroups" configuration is missing');
    }

    this.accessGroups = accessGroups;
  }

```

1. Ensure `AccessGroupsType` is imported into `src/serverSentEvent/sse.service.ts` (e.g. `import { AccessGroupsType } from '...';`).
2. Ensure `ConfigService` is imported from `@nestjs/config` if not already present.
3. If throwing on missing config is too strict for your environment, replace the `throw new Error(...)` with a safe default (e.g. `this.accessGroups = { admin: [] }`) and/or a logged warning.
</issue_to_address>

### Comment 2
<location path="src/serverSentEvent/sse.listener.ts" line_range="52-53" />
<code_context>
+      },
+    ]);
+
+    this.changeStream.on("change", (change) => this.onStream(change));
+    this.changeStream.on("error", (err) =>
+      Logger.error(`SSE change stream is closed due to error: ${err}`),
+    );
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Change stream error handling just logs and leaves the stream closed.

When an error occurs, the stream is only logged as closed but never restarted or cleared. After a transient MongoDB issue, the SSE listener will stop emitting updates for the rest of the process lifetime. Consider either retrying `watch` with backoff or at minimum nulling `this.changeStream` and exposing a health indicator so the process can detect and recover from an inactive stream.

Suggested implementation:

```typescript
  private reconnectAttempts = 0;
  private readonly maxReconnectAttempts = 5;
  private readonly baseReconnectDelayMs = 1000;

  onModuleInit() {
    this.startChangeStream();
  }

  /**
   * Initializes the MongoDB change stream and attaches listeners.
   * Can be safely called multiple times; it will only create a new stream
   * when there is no active one.
   */
  private startChangeStream() {
    if (this.changeStream) {
      return;
    }

    this.changeStream = this.connection.watch([
      {
        $match: {
          "ns.coll": { $in: Object.keys(REGISTRY) },
          operationType: { $in: Object.keys(ACTION_BY_OPERATION) },
        },
      },
    ]);

    this.changeStream.on("change", (change) => this.onStream(change));
    this.changeStream.on("error", (err) => this.onStreamError(err));

    this.reconnectAttempts = 0;
    Logger.log("SSE change stream started");
  }

  /**
   * Very simple health indicator so the process can detect an inactive stream.
   * Consider wiring this into your health endpoint / readiness probe.
   */
  isHealthy(): boolean {
    return !!this.changeStream;
  }

  /**
   * Handles change stream errors by closing the current stream, nulling
   * the reference, and scheduling a restart with bounded backoff.
   */
  private onStreamError(err: unknown) {
    Logger.error(`SSE change stream is closed due to error: ${err}`);

    try {
      this.changeStream?.close();
    } catch (closeErr) {
      Logger.error(`Failed to close SSE change stream after error: ${closeErr}`);
    } finally {
      this.changeStream = null;
    }

    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      Logger.error(
        `Max reconnect attempts (${this.maxReconnectAttempts}) reached for SSE change stream; will not restart automatically.`,
      );
      return;
    }

    this.reconnectAttempts += 1;
    const delay =
      this.baseReconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1);

    Logger.warn(
      `Scheduling SSE change stream restart (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms`,
    );

    setTimeout(() => this.startChangeStream(), delay);
  }

```

```typescript
  private onStream(change: ChangeStreamDocument) {

```

1. If your project exposes health endpoints (e.g., via `@nestjs/terminus`), you should wire `isHealthy()` into the corresponding health indicator so that an unhealthy change stream can trigger container restarts or alerts.
2. Depending on your MongoDB driver version, you may want to refine `isHealthy()` to also check `this.changeStream.closed` or similar properties if available, instead of only checking for a non-null reference.
3. If you already have configuration for retry limits/backoff, replace the hardcoded `maxReconnectAttempts` and `baseReconnectDelayMs` with values sourced from your existing config system.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/serverSentEvent/sse.service.ts Outdated
Comment thread src/serverSentEvent/sse.listener.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a Server-Sent Events (SSE) subsystem to broadcast dataset-related events to authenticated clients, backed by MongoDB change streams, with supporting updates to auth extraction and E2E MongoDB setup.

Changes:

  • Introduce global SSE module (service/controller/listener) with /events/stream and /events/connections endpoints.
  • Enable MongoDB replica set mode in E2E docker-compose to support change streams.
  • Extend JWT extraction to also accept a token query parameter (in addition to the Authorization header) and adjust mailer transport typing.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/serverSentEvent/sse.service.ts Manages SSE client connections, enforces per-user connection limits, and emits access-controlled events.
src/serverSentEvent/sse.module.ts Registers SSE components as a global Nest module.
src/serverSentEvent/sse.listener.ts Watches MongoDB change streams and converts inserts into SSE events.
src/serverSentEvent/sse.controller.ts Exposes the SSE stream endpoint and a connections monitoring endpoint.
src/serverSentEvent/interfaces/sse-registry.interface.ts Defines the registry collection-name type used by the change-stream listener.
src/auth/strategies/jwt.strategy.ts Adds query-parameter token extraction to JWT strategy.
src/app.module.ts Wires in the SSE module and refines mailer transport typing.
CI/E2E/docker-compose.yaml Enables Mongo replica set and adds a healthcheck/init logic for change streams.
CI/E2E/docker-compose-local.yaml Mirrors replica set + healthcheck/init logic for local E2E environment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/serverSentEvent/sse.controller.ts Outdated
Comment thread src/serverSentEvent/sse.service.ts Outdated
Comment thread src/auth/strategies/jwt.strategy.ts
Comment thread CI/E2E/docker-compose.yaml
Comment thread CI/E2E/docker-compose-local.yaml
Comment on lines +28 to +37
getEvents(user: JWTUser): Observable<MessageEvent> {
const userConnectionCount = [...this.clients.values()].filter(
(c) => c.user._id === user._id,
).length;

if (userConnectionCount >= this.MAX_CONNECTIONS_PER_USER_PER_INSTANCE) {
throw new ForbiddenException(
`Maximum number of ${this.MAX_CONNECTIONS_PER_USER_PER_INSTANCE} open connections reached`,
);
}
@Junjiequan Junjiequan changed the title Server sent event feat(sse-module): server sent event Jun 23, 2026
@Junjiequan
Junjiequan force-pushed the server-sent-event branch from c00dfca to 204b624 Compare July 3, 2026 08:58
@Junjiequan
Junjiequan force-pushed the server-sent-event branch from f96eb5d to d7a7c05 Compare July 6, 2026 12:55
@nitrosx

nitrosx commented Aug 19, 2026

Copy link
Copy Markdown
Member

Code Review Report: server-sent-event Branch


Overview

Commits Analyzed

The server-sent-event branch introduces 17 commits with the following key changes:

  • 81b435e: Disable real-time updates in frontend config by default
  • 964f3a0: Fix mongodb container health check
  • 457bff1: Refactor: enhance SSE event handling and registry structure
  • 9aab512: Minor refactor for jwt secret is required in production logic
  • 683b7f5: Throw error when jwt.secret is missing on production instead of using default secret
  • 3d60afa: JwtStrategy should accept query token only when it is events/stream endpoint
  • c0723d5: Correct param for isDbReplicaSet check
  • 8883e94: Added sse listener to watch mongodb changestream
  • Plus 9 earlier implementation commits

1. Logic Correctness

The SSE implementation is logically sound:

  • SseListener properly watches MongoDB change streams for registered collections (currently only Dataset)
  • SseService correctly filters events based on user access groups using canAccessByGroups
  • JWT Strategy now accepts tokens from query parameters for SSE endpoints that cannot use headers
  • Configuration enforces JWT_SECRET in production

Potential Issues:

  • sse.service.ts:65 has a debug console.log that should be removed
  • The SSE stream only handles insert operations (ACTION_BY_OPERATION only maps "insert" to "created")
  • No handling for update and delete operations despite being defined in SseAction type

2. Edge Cases

  • Handled: MongoDB replica set check prevents SSE from starting if not supported
  • Handled: Maximum connections per user (5) prevents resource exhaustion
  • Handled: Automatic reconnection with exponential backoff (max 5 attempts)
  • Handled: Token extraction from query params only for /events/stream path
  • Not Handled: What happens when accessGroups config is undefined in SseService (line 23-28)
  • Not Handled: No rate limiting on SSE connections
  • Not Handled: No cleanup of stale connections (only on module destroy)

3. What the Code Does

The changes implement a Server-Sent Events (SSE) system that:

  • Allows clients to subscribe to real-time notifications via /api/events/stream
  • Pushes events when datasets are created (inserted into MongoDB)
  • Filters events based on user's access groups (ownerGroup, accessGroups, admin groups)
  • Provides a monitoring endpoint at /api/events/connections for active connections
  • Requires MongoDB replica set (for change streams capability)
  • Integrates with existing CASL authorization system

4. Assessment of Changes

The changes make sense and follow NestJS patterns:

  • Module structure is clean (controller, service, listener)
  • Uses RxJS Observables for streaming
  • Properly integrates with existing auth (JWT) and authorization (CASL) systems
  • TypeScript types are well-defined
  • Configuration is flexible

5. Unreachable Code

  • sse-registry.interface.ts: Defines WatchableCollection for 12 collections but only Dataset is registered in REGISTRY (line 17 of sse.listener.ts)
  • SseAction type: Defines "updated" and "deleted" but only "created" is used (ACTION_BY_OPERATION only maps insert)
  • **sse.controller.ts:54-57: Policy check for connections endpoint uses placeholder comment TODO


Code Changes

Files Changed (12 files, 457 insertions, 10 deletions)

File Change Type Description
CI/E2E/docker-compose-local.yaml Modified Added MongoDB replica set configuration and health check
CI/E2E/docker-compose.yaml Modified Added MongoDB replica set configuration and health check
src/app.module.ts Modified Added SseModule import and registration, fixed MailerOptions type
src/auth/strategies/jwt.strategy.ts Modified Added token extraction from query params for SSE endpoint
src/config/configuration.ts Modified Added JWT_SECRET production validation
src/config/frontend.config.json Modified Added realTimeUpdatesEnabled: false
src/serverSentEvent/interfaces/sse-event.interface.ts New Defines SSE client, event, and connection report interfaces
src/serverSentEvent/interfaces/sse-registry.interface.ts New Defines watchable collections and registry entries
src/serverSentEvent/sse.controller.ts New SSE endpoint controller with streaming and connections endpoints
src/serverSentEvent/sse.listener.ts New MongoDB change stream listener
src/serverSentEvent/sse.module.ts New SSE feature module
src/serverSentEvent/sse.service.ts New Core SSE service for managing clients and emitting events

Assessment

Necessary Changes:

  • ✅ JWT production validation (security critical)
  • ✅ MongoDB replica set support in CI (required for change streams)
  • ✅ SSE module structure (clean architecture)
  • ✅ Token extraction from query params (enables EventSource API usage)

Questionable Changes:

  • ⚠️ app.module.ts type import change: Changed from TransportType to MailerOptions["transport"] - this is a minor improvement but not directly related to SSE

Improvement Needed

  1. Remove debug logging: sse.service.ts:65 has console.log(this.canUserAccess(user, event.message)) that should be removed or replaced with proper logging
  2. Implement all action types: The ACTION_BY_OPERATION map only handles "insert" but the type defines "created", "updated", "deleted"
  3. Expand registry: Only Dataset collection is watched; consider adding other collections from WatchableCollection
  4. Connection cleanup: Add TTL or heartbeat mechanism to detect and cleanup stale connections
  5. Rate limiting: Add rate limiting for SSE connections to prevent abuse
  6. Error handling: The onStreamError method logs errors but doesn't distinguish between recoverable and non-recoverable errors
  7. Type safety: In sse.service.ts:103-109, this.accessGroups?.admin could be undefined - add null check
  8. Policy for connections endpoint: The TODO comment at sse.controller.ts:54 needs resolution
  9. Test coverage: No tests exist for the SSE module (critical gap)
  10. Configuration: realTimeUpdatesEnabled is hardcoded to false in frontend config - should be configurable

Verdict

Positive: The SSE implementation is well-structured, follows NestJS best practices, and correctly integrates with existing systems. The MongoDB change stream approach is appropriate for real-time notifications.

Needs Work: The implementation is incomplete (only handles inserts for Datasets) and lacks test coverage. The presence of debug logging and TODOs indicates it's not production-ready.

Overall: The changes are a good foundation for SSE functionality but require additional work before being merged to master.



Security Review

1. Potential Injection Vulnerabilities

None identified in the SSE-specific code. However:

  • The MongoDB change stream filter uses Object.keys(REGISTRY) which is safe (static object)
  • The token extraction from query params uses proper type checking (typeof token === "string")

Verdict: No injection vulnerabilities in the new code.

2. Sensitive User Data Exposure

Potential Issue:

  • sse.controller.ts:60-61: The /events/connections endpoint returns all active connections with usernames. This could expose:
    • Which users are currently connected
    • Connection patterns and usage
  • Mitigation: The endpoint is protected by PoliciesGuard with RuntimeConfigUpdateEndpoint policy, but the policy itself may be too permissive

Verdict: The connections endpoint exposes user information but is protected by authorization. However, the policy check uses a placeholder TODO comment, indicating it needs proper policy definition.

3. Insecure API Usage

Issues Found:

  • Token in URL (Medium Risk): jwt.strategy.ts:11-17 - Tokens can be passed via query parameters (?token=xxx) for the SSE endpoint. This is insecure because:

    • Query parameters are logged in server logs
    • Query parameters appear in browser history
    • Query parameters may be leaked via Referer headers
    • Commit responsible: 3d60afa
  • No Token Expiration in URL: While the strategy has ignoreExpiration: false, tokens in URLs have longer exposure windows

Verdict: The query parameter token approach is a security anti-pattern, though it's limited to the /events/stream endpoint only.

4. Authentication Bypass

Potential Issue:

  • The fromSseQueryAsBearerToken extractor (lines 11-17 of jwt.strategy.ts) accepts tokens from query parameters without any additional validation beyond the path check
  • If an attacker can manipulate the request path or if there's a path traversal vulnerability, they could potentially use query tokens elsewhere
  • Mitigation: The path check is strict (req.path.endsWith("events/stream")), which is good

Verdict: No direct bypass identified, but the query parameter token approach increases the attack surface.



Test Coverage

Current State

No tests exist for the SSE module. This is a critical gap.

Files Missing Tests

  • sse.service.ts - No tests
  • sse.controller.ts - No tests
  • sse.listener.ts - No tests
  • sse.module.ts - No tests

Suggested Tests

  1. SseService Tests:

    • Test getEvents() returns observable and registers client
    • Test emit() filters events based on user access groups
    • Test canAccessByGroups() with various group configurations
    • Test connection limit enforcement (max 5 per user)
    • Test getAllConnections() returns correct report
    • Test cleanup on module destroy
  2. SseController Tests:

    • Test SSE stream endpoint authentication
    • Test connections endpoint authorization
    • Test that stream returns proper observable
  3. SseListener Tests:

    • Test change stream initialization when DB is replica set
    • Test change stream NOT initialized when DB is not replica set
    • Test onChange() properly emits events for registered collections
    • Test onChange() ignores events for unregistered collections
    • Test onStreamError() reconnection logic
    • Test max reconnection attempts limit
  4. JWT Strategy Tests:

    • Test token extraction from Authorization header
    • Test token extraction from query param for SSE endpoint
    • Test token NOT extracted from query param for non-SSE endpoints
  5. Integration Tests:

    • Test full flow: MongoDB insert → change stream → SSE event → client receives filtered event
    • Test with multiple users and different access groups


Security Examples

1. Token in URL - Real-Life Example

Vulnerability: Bearer tokens in query parameters

Example Attack:

User connects to: https://scicat.example.com/api/events/stream?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Server logs: GET /api/events/stream?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Impact:

  • Admin reviewing logs can see user tokens
  • If logs are compromised, all active tokens are exposed
  • Tokens may persist in proxy server logs, CDN logs, etc.

Fix: Use only Authorization header. For EventSource API, consider:

  • Using a short-lived token specifically for SSE
  • Implementing a cookie-based approach
  • Using a separate authentication mechanism for SSE

2. Information Disclosure via Connections Endpoint

Vulnerability: Exposing active user connections

Example Attack:

GET /api/events/connections
Response: {"connections": 15, "users": {"admin": 1, "user1": 3, "user2": 11}}

Impact:

  • Attacker can enumerate active users
  • Can determine usage patterns
  • Can identify when specific users are online

Fix: Restrict the connections endpoint to admin-only access with proper policy definition.



Testing for Security Use Cases

1. Testing for Token in URL Vulnerability

Test File: src/auth/strategies/jwt.strategy.spec.ts

describe('JWT Strategy - Query Token Security', () => {
  it('should NOT extract token from query params for non-SSE endpoints', () => {
    const req = {
      path: '/api/datasets',
      query: { token: 'test-token' }
    } as Request;

    const result = fromSseQueryAsBearerToken(req);
    expect(result).toBeNull();
  });

  it('should extract token from query params only for /events/stream', () => {
    const req = {
      path: '/api/events/stream',
      query: { token: 'test-token' }
    } as Request;

    const result = fromSseQueryAsBearerToken(req);
    expect(result).toBe('test-token');
  });

  it('should not extract token if query param is array', () => {
    const req = {
      path: '/api/events/stream',
      query: { token: ['token1', 'token2'] }
    } as Request;

    const result = fromSseQueryAsBearerToken(req);
    expect(result).toBeNull();
  });
});

Code Affected: src/auth/strategies/jwt.strategy.ts:11-17

2. Testing for Information Disclosure

Test File: src/serverSentEvent/sse.controller.spec.ts

describe('SSE Controller - Connections Endpoint Security', () => {
  it('should deny access to connections endpoint for non-admin users', () => {
    // Setup mock user without admin privileges
    const user = { _id: '1', username: 'user', currentGroups: ['pgroup1'] };

    // Mock the policies guard to check policy
    // Expect 403 Forbidden
  });

  it('should allow access to connections endpoint for admin users', () => {
    // Setup mock admin user
    const user = { _id: '1', username: 'admin', currentGroups: ['admin'] };

    // Expect 200 OK with connections data
  });
});

Code Affected: src/serverSentEvent/sse.controller.ts:54-61

3. Testing Access Control Filtering

Test File: src/serverSentEvent/sse.service.spec.ts

describe('SSE Service - Access Control', () => {
  it('should not send events for datasets user cannot access', () => {
    const user = { _id: '1', username: 'user', currentGroups: ['pgroup1'] };
    const dataset = {
      ownerGroup: 'pgroup2',
      accessGroups: ['pgroup3']
    };

    // Setup service with mock accessGroups config
    // Emit event for dataset
    // Verify user's subject did NOT receive the event
  });

  it('should send events for datasets user owns', () => {
    const user = { _id: '1', username: 'user', currentGroups: ['pgroup1'] };
    const dataset = {
      ownerGroup: 'pgroup1',
      accessGroups: []
    };

    // Emit event for dataset
    // Verify user's subject DID receive the event
  });

  it('should send events for datasets in user accessGroups', () => {
    const user = { _id: '1', username: 'user', currentGroups: ['pgroup1'] };
    const dataset = {
      ownerGroup: 'pgroup2',
      accessGroups: ['pgroup1']
    };

    // Emit event for dataset
    // Verify user's subject DID receive the event
  });
});

Code Affected: src/serverSentEvent/sse.service.ts:103-124



Summary

Key Findings

  1. Feature Implementation: The SSE feature is well-architected but incomplete (only handles Dataset inserts, not updates/deletes or other collections)

  2. Critical Security Issue: Tokens passed via query parameters for SSE endpoint create logging and exposure risks (commit 3d60afa)

  3. Missing Tests: No test coverage for the entire SSE module - critical gap before production

  4. Configuration: JWT_SECRET production validation is a positive security improvement (commits 683b7f5, 9aab512)

  5. Infrastructure: MongoDB replica set configuration in CI is correct for change streams

  6. Debug Code: Production code contains console.log (sse.service.ts:65) that must be removed

Recommendations

  1. Before Merge:

    • Remove query parameter token support or implement a more secure alternative
    • Remove all debug logging
    • Add comprehensive test coverage
    • Resolve the TODO for connections endpoint policy
    • Define proper policies for SSE endpoints
  2. After Merge:

    • Expand registry to support more collections
    • Implement update and delete event handling
    • Add rate limiting
    • Add connection heartbeat/cleanup
  3. Security Hardening:

    • Audit all endpoints for token handling patterns
    • Review logging configuration to ensure tokens are not logged
    • Consider implementing token rotation for SSE connections

Attribution

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe vibe@mistral.ai

@nitrosx nitrosx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Few minor comments.
Please also check the AI review.

About tests: should we create an additional PR with them?

Comment thread src/auth/strategies/jwt.strategy.ts Outdated
Comment on lines +11 to +18
const fromSseQueryAsBearerToken = (req: Request): string | null => {
if (req.path.endsWith("events/stream")) {
const token = req.query?.token;
return typeof token === "string" ? token : null;
}
return null;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please check the AI review regarding passing the token on the URI.

Comment thread src/config/configuration.ts Outdated
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret && process.env.NODE_ENV === "production") {
throw new Error("JWT_SECRET is required in production");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why only for production?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it's because we can simply use fallback for development environment, while for production one must set it explicitly assuming NODE_ENV is provided

- Introduced WatchableCollection type for MongoDB collections in SSE.
- Updated SseRegistryEntry interface to include entity and DTO mapping.
- Improved SseListener to handle change streams with reconnection logic.
- Enhanced SseService to manage client connections and access control.
- Added SseClient and SseEvent interfaces for better type safety.
Changed realTimeUpdatesEnabled from true to false.
EventSource cannot send auth headers, so mint a short-lived ticket here
and pass it on the stream URL instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants