"use node"; import { action } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import { requireAuth } from "./authGuard"; /** * Signed-send: the client signs `send:${channelId}:${senderId}:${timestamp}` * with their Ed25519 key so the server can prove the caller controls * `senderId`. Prevents the "anyone posts as anyone" bypass that existed * when `messages.send` was a plain mutation trusting the client-supplied * `senderId`. * * The per-message `signature` over the ciphertext is a separate, * recipient-verified integrity check and is unchanged. */ export const send = action({ args: { channelId: v.id("channels"), senderId: v.id("userProfiles"), ciphertext: v.string(), nonce: v.string(), signature: v.string(), keyVersion: v.number(), replyTo: v.optional(v.id("messages")), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.object({ id: v.id("messages") }), handler: async (ctx, args): Promise<{ id: any }> => { const canonical = `send:${args.channelId}:${args.senderId}:${args.authTimestamp}`; await requireAuth(ctx, args.senderId, args.authTimestamp, args.authSignature, canonical); return await ctx.runMutation(internal.messages.sendInternal, { channelId: args.channelId, senderId: args.senderId, ciphertext: args.ciphertext, nonce: args.nonce, signature: args.signature, keyVersion: args.keyVersion, replyTo: args.replyTo, }); }, }); export const edit = action({ args: { id: v.id("messages"), userId: v.id("userProfiles"), ciphertext: v.string(), nonce: v.string(), signature: v.string(), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.null(), handler: async (ctx, args): Promise => { const canonical = `edit:${args.id}:${args.userId}:${args.authTimestamp}`; await requireAuth(ctx, args.userId, args.authTimestamp, args.authSignature, canonical); await ctx.runMutation(internal.messages.editInternal, { id: args.id, userId: args.userId, ciphertext: args.ciphertext, nonce: args.nonce, signature: args.signature, }); return null; }, }); export const pin = action({ args: { id: v.id("messages"), userId: v.id("userProfiles"), pinned: v.boolean(), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.null(), handler: async (ctx, args): Promise => { const canonical = `pin:${args.id}:${args.userId}:${args.pinned}:${args.authTimestamp}`; await requireAuth(ctx, args.userId, args.authTimestamp, args.authSignature, canonical); await ctx.runMutation(internal.messages.pinInternal, { id: args.id, userId: args.userId, pinned: args.pinned, }); return null; }, }); export const remove = action({ args: { id: v.id("messages"), userId: v.id("userProfiles"), authTimestamp: v.number(), authSignature: v.string(), }, returns: v.null(), handler: async (ctx, args): Promise => { const canonical = `remove:${args.id}:${args.userId}:${args.authTimestamp}`; await requireAuth(ctx, args.userId, args.authTimestamp, args.authSignature, canonical); await ctx.runMutation(internal.messages.removeInternal, { id: args.id, userId: args.userId, }); return null; }, });