diff --git a/apps/backend/src/__tests__/conversations.cache.test.ts b/apps/backend/src/__tests__/conversations.cache.test.ts
index 7e7f679..366d712 100644
--- a/apps/backend/src/__tests__/conversations.cache.test.ts
+++ b/apps/backend/src/__tests__/conversations.cache.test.ts
@@ -62,7 +62,7 @@ vi.mock('../db/schema.js', () => ({
id: 'id',
conversationId: 'conversationId',
senderId: 'senderId',
- content: 'content',
+ ciphertext: 'ciphertext',
createdAt: 'createdAt',
deletedAt: 'deletedAt',
},
@@ -189,51 +189,6 @@ describe('GET /conversations — Redis caching', () => {
});
});
-describe('GET /conversations/:id/search', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- mockRedisInstance = { get: mockGet, setex: mockSetex, del: mockDel };
- });
-
- it('returns 400 when the query is empty', async () => {
- const res = await request(makeApp()).get('/conversations/conv-1/search?q= ');
-
- expect(res.status).toBe(400);
- expect(mockFindFirst).not.toHaveBeenCalled();
- expect(mockExecute).not.toHaveBeenCalled();
- });
-
- it('returns 403 when the user is not a conversation member', async () => {
- mockFindFirst.mockResolvedValue(undefined);
-
- const res = await request(makeApp()).get('/conversations/conv-1/search?q=hello');
-
- expect(res.status).toBe(403);
- expect(mockExecute).not.toHaveBeenCalled();
- });
-
- it('returns ranked highlighted matches for conversation members', async () => {
- const searchResults = [
- {
- id: 'msg-1',
- conversationId: 'conv-1',
- senderId: TEST_USER_ID,
- content: 'hello from stellar',
- snippet: 'hello from stellar',
- rank: '0.1',
- },
- ];
- mockFindFirst.mockResolvedValue({ id: 'member-1' });
- mockExecute.mockResolvedValue(searchResults);
-
- const res = await request(makeApp()).get('/conversations/conv-1/search?q=hello');
-
- expect(res.status).toBe(200);
- expect(res.body).toEqual({ results: searchResults });
- expect(mockExecute).toHaveBeenCalledTimes(1);
- });
-});
-
describe('GET /conversations — isArchived filter', () => {
beforeEach(() => {
vi.clearAllMocks();
diff --git a/apps/backend/src/__tests__/conversations.routes.test.ts b/apps/backend/src/__tests__/conversations.routes.test.ts
index f402138..aec47d1 100644
--- a/apps/backend/src/__tests__/conversations.routes.test.ts
+++ b/apps/backend/src/__tests__/conversations.routes.test.ts
@@ -54,7 +54,7 @@ vi.mock('../db/schema.js', () => ({
id: 'id',
conversationId: 'conversationId',
senderId: 'senderId',
- content: 'content',
+ ciphertext: 'ciphertext',
createdAt: 'createdAt',
deletedAt: 'deletedAt',
},
@@ -138,7 +138,7 @@ describe('GET /conversations/:id', () => {
id: 'msg-1',
conversationId: 'conv-1',
senderId: 'user-1',
- content: 'hello',
+ ciphertext: 'hello',
deletedAt: null,
sender: {
id: 'user-1',
@@ -157,7 +157,7 @@ describe('GET /conversations/:id', () => {
expect(res.status).toBe(200);
expect(res.body.id).toBe('conv-1');
expect(res.body.messages).toHaveLength(1);
- expect(res.body.messages[0].content).toBe('hello');
+ expect(res.body.messages[0].ciphertext).toBe('hello');
});
});
diff --git a/apps/backend/src/__tests__/messages.routes.test.ts b/apps/backend/src/__tests__/messages.routes.test.ts
index f48ae04..e39f0c6 100644
--- a/apps/backend/src/__tests__/messages.routes.test.ts
+++ b/apps/backend/src/__tests__/messages.routes.test.ts
@@ -41,7 +41,7 @@ vi.mock('../db/schema.js', () => ({
id: 'id',
conversationId: 'conversationId',
senderId: 'senderId',
- content: 'content',
+ ciphertext: 'ciphertext',
createdAt: 'createdAt',
deletedAt: 'deletedAt',
},
@@ -83,7 +83,7 @@ describe('DELETE /messages/:id', () => {
id: 'msg-1',
conversationId: 'conv-1',
senderId: 'user-2',
- content: 'hello',
+ ciphertext: 'hello',
deletedAt: null,
});
@@ -98,7 +98,7 @@ describe('DELETE /messages/:id', () => {
id: 'msg-1',
conversationId: 'conv-1',
senderId: 'user-1',
- content: 'hello',
+ ciphertext: 'hello',
deletedAt: null,
});
diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts
index 9e09e99..5ba4fce 100644
--- a/apps/backend/src/db/schema.ts
+++ b/apps/backend/src/db/schema.ts
@@ -67,16 +67,10 @@ export const messages = pgTable(
senderId: uuid('sender_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
- content: text('content').notNull(),
+ ciphertext: text('ciphertext'),
createdAt: timestamp('created_at').notNull().defaultNow(),
deletedAt: timestamp('deleted_at'),
},
- (table) => [
- index('messages_content_search_idx').using(
- 'gin',
- sql`to_tsvector('english', ${table.content})`,
- ),
- ],
);
// ─── Devices & prekeys (issues #158, #159, #162) ─────────────────────────────
diff --git a/apps/backend/src/lib/messages.ts b/apps/backend/src/lib/messages.ts
index a07cb4c..b2b6e2a 100644
--- a/apps/backend/src/lib/messages.ts
+++ b/apps/backend/src/lib/messages.ts
@@ -1,15 +1,15 @@
type MessageLike = {
- content: string | null;
+ ciphertext: string | null;
deletedAt?: Date | null;
};
export function serializeMessage(
message: T,
-): Omit & { content: string | null } {
+): Omit & { ciphertext: string | null } {
const { deletedAt, ...rest } = message;
return {
...rest,
- content: deletedAt ? null : message.content,
+ ciphertext: deletedAt ? null : message.ciphertext,
};
}
diff --git a/apps/backend/src/routes/conversations.ts b/apps/backend/src/routes/conversations.ts
index 822070f..89bb61e 100644
--- a/apps/backend/src/routes/conversations.ts
+++ b/apps/backend/src/routes/conversations.ts
@@ -14,8 +14,6 @@ export const conversationsRouter: IRouter = Router();
conversationsRouter.use(requireAuth);
-const SEARCH_RESULT_LIMIT = 20;
-
const conversationRelations = {
members: {
with: {
@@ -485,69 +483,6 @@ conversationsRouter.get('/:id/messages', async (req: AuthRequest, res) => {
res.json({ messages: page, nextCursor });
});
-conversationsRouter.get('/:id/search', async (req: AuthRequest, res) => {
- const userId = req.auth!.userId;
- const conversationId = req.params['id'] as string | undefined;
- const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
-
- if (!conversationId) {
- res.status(400).json({ error: 'Conversation id is required' });
- return;
- }
-
- if (!query) {
- res.status(400).json({ error: 'Search query is required' });
- return;
- }
-
- const membership = await db.query.conversationMembers.findFirst({
- where: and(
- eq(conversationMembers.conversationId, conversationId),
- eq(conversationMembers.userId, userId),
- ),
- });
-
- if (!membership) {
- res.status(403).json({ error: 'Not a member of this conversation' });
- return;
- }
-
- const results = await db.execute<{
- id: string;
- conversationId: string;
- senderId: string;
- content: string;
- createdAt: Date;
- snippet: string;
- rank: string;
- }>(sql`
- WITH search_query AS (
- SELECT websearch_to_tsquery('english', ${query}) AS query
- )
- SELECT
- ${messages.id} AS "id",
- ${messages.conversationId} AS "conversationId",
- ${messages.senderId} AS "senderId",
- ${messages.content} AS "content",
- ${messages.createdAt} AS "createdAt",
- ts_headline(
- 'english',
- ${messages.content},
- search_query.query,
- 'StartSel=, StopSel=, MaxWords=24, MinWords=8, ShortWord=3, HighlightAll=false'
- ) AS "snippet",
- ts_rank_cd(to_tsvector('english', ${messages.content}), search_query.query) AS "rank"
- FROM ${messages}, search_query
- WHERE ${messages.conversationId} = ${conversationId}
- AND ${messages.deletedAt} IS NULL
- AND search_query.query @@ to_tsvector('english', ${messages.content})
- ORDER BY "rank" DESC, ${messages.createdAt} DESC
- LIMIT ${SEARCH_RESULT_LIMIT}
- `);
-
- res.json({ results });
-});
-
// PATCH /conversations/:id/settings — update muted/archived state for the authenticated user
conversationsRouter.patch('/:id/settings', async (req: AuthRequest, res) => {
const userId = req.auth!.userId;
diff --git a/apps/backend/src/socket/messaging.ts b/apps/backend/src/socket/messaging.ts
index 17d3bab..5c22d28 100644
--- a/apps/backend/src/socket/messaging.ts
+++ b/apps/backend/src/socket/messaging.ts
@@ -35,13 +35,13 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void
});
// ── send_message ───────────────────────────────────────────────────────────
- // Payload: { conversationId: string; content: string }
+ // Payload: { conversationId: string; ciphertext: string }
// Persists the message and broadcasts it to all room members.
- socket.on('send_message', async (payload: { conversationId: string; content: string }) => {
- const { conversationId, content } = payload;
+ socket.on('send_message', async (payload: { conversationId: string; ciphertext: string }) => {
+ const { conversationId, ciphertext } = payload;
- if (!content?.trim()) {
- socket.emit('error', { event: 'send_message', message: 'Content must not be empty' });
+ if (!ciphertext?.trim()) {
+ socket.emit('error', { event: 'send_message', message: 'Ciphertext must not be empty' });
return;
}
@@ -59,7 +59,7 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void
const [message] = await db
.insert(messages)
- .values({ conversationId, senderId: userId, content: content.trim() })
+ .values({ conversationId, senderId: userId, ciphertext: ciphertext.trim() })
.returning();
io.to(conversationId).emit('new_message', message);
@@ -238,15 +238,15 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void
});
// ── ask_assistant ──────────────────────────────────────────────────────────
- // Payload: { conversationId: string; content: string }
+ // Payload: { conversationId: string; ciphertext: string }
// Forwards to AI agent and posts reply from reserved assistant user.
// Rate-limit: 5 requests per user per minute.
const ASSISTANT_USER_ID = '00000000-0000-4000-8000-000000000000';
- socket.on('ask_assistant', async (payload: { conversationId: string; content: string }) => {
- const { conversationId, content } = payload;
+ socket.on('ask_assistant', async (payload: { conversationId: string; ciphertext: string }) => {
+ const { conversationId, ciphertext } = payload;
- if (!content?.trim().startsWith('@assistant')) {
+ if (!ciphertext?.trim().startsWith('@assistant')) {
return;
}
@@ -284,7 +284,7 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- message: content,
+ message: ciphertext,
conversation_id: conversationId,
}),
});
@@ -317,7 +317,7 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void
.values({
conversationId,
senderId: ASSISTANT_USER_ID,
- content: data.reply,
+ ciphertext: data.reply,
})
.returning();