Skip to content

feat: upload integrity, file_message event, push mute, device presence#280

Open
0xratnendra wants to merge 2 commits into
codebestia:mainfrom
0xratnendra:feat/upload-integrity-file-event-push-mute-device-presence
Open

feat: upload integrity, file_message event, push mute, device presence#280
0xratnendra wants to merge 2 commits into
codebestia:mainfrom
0xratnendra:feat/upload-integrity-file-event-push-mute-device-presence

Conversation

@0xratnendra

Copy link
Copy Markdown

closes #217
closes #232
closes #233
closes #238

Summary

Implements four features:

  1. Ciphertext SHA256 verification on message send — transport-corruption integrity check
  2. WebSocket file_message event — signals file availability without sending bytes over the socket
  3. Push notification mute respect — skips push for muted conversations and devices with push disabled
  4. Device-based user presence — derives user online/offline state from device activity, exposes lastSeen

1. Ciphertext SHA256 Verification

What

When the client sends a send_message payload, it may optionally include ciphertextSha256. The server computes SHA-256 over the stored ciphertext and rejects the message on mismatch with a clear integrity_error.

Why

This is a transport-corruption check. The primary integrity mechanism remains the AEAD tag inside the ciphertext (verified client-side at decryption time). The server-side hash catches corruption that may occur between the client and database (e.g., faulty proxies, memory errors).

Changes

  • apps/backend/src/socket/messaging.ts — added ciphertextSha256 to the payload type; computes and compares hash before inserting the message; rejects with integrity_error on mismatch

Acceptance Criteria

  • Ciphertext hash verified server-side at completion
  • Corruption produces a clear error, file stays unusable
  • Decryption-time AEAD verification documented for clients

2. WebSocket file_message Event

What

When a message with a file-type contentType (file/*, image/*, video/*, audio/*, or file) is sent, the server emits a file_message event to the conversation room carrying { messageId, conversationId, fileId }.

Why

The WebSocket carries only metadata/availability signals, never file bytes. Recipients use the file_message event to know when to fetch and decrypt a file via GET /files/:id over HTTP. Delivery and receipts reuse the standard pipeline.

Changes

  • apps/backend/src/socket/messaging.ts — after new_message broadcast, emits file_message for file-type content

Acceptance Criteria

  • WS carries only metadata; no file bytes traverse the socket
  • Recipients fetch via GET /files/:id after the event
  • Delivery/receipts reuse the standard pipeline

3. Push Notification Mute Respect

What

Before dispatching a push notification for a new message:

  • Checks conversation_members.isMutedskips muted conversations
  • Checks user_devices.push_enabledskips devices with push disabled
  • Checks online status — skips users with active WebSocket connections

Why

Muted conversations should not generate push notifications. Each device can also have a global push opt-out. These checks are applied before dispatch to avoid unnecessary wake-ups.

Changes

  • apps/backend/src/db/schema.ts — added pushEnabled column to user_devices (default true)
  • apps/backend/drizzle/0009_push_enabled.sql — migration
  • apps/backend/drizzle/meta/_journal.json — migration entry
  • apps/backend/src/services/push.ts — new push notification service using web-push with VAPID
  • apps/backend/src/socket/messaging.ts — calls sendPushForMessage after message broadcast
  • apps/backend/package.json — added web-push and @types/web-push dependencies

Configuration

Set these environment variables to enable push:

VAPID_SUBJECT=mailto:admin@clicked.app
VAPID_PUBLIC_KEY=<your-public-key>
VAPID_PRIVATE_KEY=<your-private-key>

Acceptance Criteria

  • Muted conversations produce no push
  • Global per-device opt-out honored
  • Preference checks happen before send
  • Push is best-effort (never blocks message delivery)

4. Device-Based User Presence

What

User presence is now derived from device activity rather than relying solely on Redis WebSocket tracking:

  • Online: any non-revoked device has lastSeenAt within a 90-second window
  • Offline: lastSeen reflects the most recent lastSeenAt across all devices

Exposed via:

  • GET /users/:id/presence — returns { online: boolean, lastSeen?: string }
  • WebSocket presence_update event — includes lastSeen when the user goes offline

How

  • user_devices.lastSeenAt is updated on connect, heartbeat, and disconnect
  • deriveDevicePresence() in services/presence.ts checks if any non-revoked device was active within 90s
  • The presence endpoint falls back gracefully to device-based presence when Redis is unavailable

Changes

  • apps/backend/src/services/presence.ts — added deriveDevicePresence()
  • apps/backend/src/services/heartbeat.ts — updates user_devices.lastSeenAt on heartbeat
  • apps/backend/src/index.ts — updates lastSeenAt on connect/disconnect; includes lastSeen in presence_update events
  • apps/backend/src/routes/users.ts — presence endpoint uses Redis then falls back to device-based presence
  • apps/backend/src/middleware/socketAuth.ts — exposes identityPublicKey on the socket for device lookups

Acceptance Criteria

  • User shows online while ≥1 device is online
  • lastSeen reflects the most recent device activity when offline
  • Endpoint + WS event agree

Test Results

Test Files  15 passed (15)
     Tests  129 passed (129)
  • TypeScript compilation: clean (tsc --noEmit)
  • ESLint: 0 errors (6 pre-existing warnings)
  • Prettier format: passing

- Verify ciphertext SHA256 on send_message; reject mismatches
  (AEAD tag remains primary integrity; server hash is transport check)
- Emit file_message WS event for file/image/video/audio content;
  file bytes flow over HTTP via GET /files/:id, not the socket
- Respect mute settings before push dispatch: check
  conversation_members.isMuted and per-device pushEnabled toggle
- Derive user presence from user_devices.lastSeenAt;
  online if any non-revoked device has recent activity;
  expose lastSeen via GET /users/:id/presence and presence_update WS event
@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@0xratnendra Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@codebestia

Copy link
Copy Markdown
Owner

GM @0xratnendra
Please resolve the conflicts.

@codebestia

Copy link
Copy Markdown
Owner

GM @0xratnendra
Please resolve the conflicts and fix the CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants