Claude Agent Skill · by Waynesutton

Convex Security Audit

Convex Security Audit provides developers building on the Convex platform with comprehensive patterns and utilities for reviewing and implementing security cont

convexsecurityauditauthorizationrate-limitingprotection
Install
Terminal · npx
$npx skills add https://github.com/waynesutton/convexskills --skill convex-security-audit
Works with Paperclip

How Convex Security Audit fits into a Paperclip company.

Convex Security Audit drops into any Paperclip agent that handles convex and security work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md539 lines
Expand
---name: convex-security-auditdisplayName: Convex Security Auditdescription: Deep security review patterns for authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operationsversion: 1.0.0author: Convextags: [convex, security, audit, authorization, rate-limiting, protection]--- # Convex Security Audit Comprehensive security review patterns for Convex applications including authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations. ## Documentation Sources Before implementing, do not assume; fetch the latest documentation: - Primary: https://docs.convex.dev/auth/functions-auth- Production Security: https://docs.convex.dev/production- For broader context: https://docs.convex.dev/llms.txt ## Instructions ### Security Audit Areas 1. **Authorization Logic** - Who can do what2. **Data Access Boundaries** - What data users can see3. **Action Isolation** - Protecting external API calls4. **Rate Limiting** - Preventing abuse5. **Sensitive Operations** - Protecting critical functions ### Authorization Logic Audit #### Role-Based Access Control (RBAC) ```typescript// convex/lib/auth.tsimport { QueryCtx, MutationCtx } from "./_generated/server";import { ConvexError } from "convex/values";import { Doc } from "./_generated/dataModel"; type UserRole = "user" | "moderator" | "admin" | "superadmin"; const roleHierarchy: Record<UserRole, number> = {  user: 0,  moderator: 1,  admin: 2,  superadmin: 3,}; export async function getUser(ctx: QueryCtx | MutationCtx): Promise<Doc<"users"> | null> {  const identity = await ctx.auth.getUserIdentity();  if (!identity) return null;    return await ctx.db    .query("users")    .withIndex("by_tokenIdentifier", (q) =>       q.eq("tokenIdentifier", identity.tokenIdentifier)    )    .unique();} export async function requireRole(  ctx: QueryCtx | MutationCtx,   minRole: UserRole): Promise<Doc<"users">> {  const user = await getUser(ctx);    if (!user) {    throw new ConvexError({      code: "UNAUTHENTICATED",      message: "Authentication required",    });  }    const userRoleLevel = roleHierarchy[user.role as UserRole] ?? 0;  const requiredLevel = roleHierarchy[minRole];    if (userRoleLevel < requiredLevel) {    throw new ConvexError({      code: "FORBIDDEN",      message: `Role '${minRole}' or higher required`,    });  }    return user;} // Permission-based checktype Permission = "read:users" | "write:users" | "delete:users" | "admin:system"; const rolePermissions: Record<UserRole, Permission[]> = {  user: ["read:users"],  moderator: ["read:users", "write:users"],  admin: ["read:users", "write:users", "delete:users"],  superadmin: ["read:users", "write:users", "delete:users", "admin:system"],}; export async function requirePermission(  ctx: QueryCtx | MutationCtx,  permission: Permission): Promise<Doc<"users">> {  const user = await getUser(ctx);    if (!user) {    throw new ConvexError({ code: "UNAUTHENTICATED", message: "Authentication required" });  }    const userRole = user.role as UserRole;  const permissions = rolePermissions[userRole] ?? [];    if (!permissions.includes(permission)) {    throw new ConvexError({      code: "FORBIDDEN",      message: `Permission '${permission}' required`,    });  }    return user;}``` ### Data Access Boundaries Audit ```typescript// convex/data.tsimport { query, mutation } from "./_generated/server";import { v } from "convex/values";import { getUser, requireRole } from "./lib/auth";import { ConvexError } from "convex/values"; // Audit: Users can only see their own dataexport const getMyData = query({  args: {},  returns: v.array(v.object({    _id: v.id("userData"),    content: v.string(),  })),  handler: async (ctx) => {    const user = await getUser(ctx);    if (!user) return [];        // SECURITY: Filter by userId    return await ctx.db      .query("userData")      .withIndex("by_user", (q) => q.eq("userId", user._id))      .collect();  },}); // Audit: Verify ownership before returning sensitive dataexport const getSensitiveItem = query({  args: { itemId: v.id("sensitiveItems") },  returns: v.union(v.object({    _id: v.id("sensitiveItems"),    secret: v.string(),  }), v.null()),  handler: async (ctx, args) => {    const user = await getUser(ctx);    if (!user) return null;        const item = await ctx.db.get(args.itemId);        // SECURITY: Verify ownership    if (!item || item.ownerId !== user._id) {      return null; // Don't reveal if item exists    }        return item;  },}); // Audit: Shared resources with access listexport const getSharedDocument = query({  args: { docId: v.id("documents") },  returns: v.union(v.object({    _id: v.id("documents"),    content: v.string(),    accessLevel: v.string(),  }), v.null()),  handler: async (ctx, args) => {    const user = await getUser(ctx);    const doc = await ctx.db.get(args.docId);        if (!doc) return null;        // Public documents    if (doc.visibility === "public") {      return { ...doc, accessLevel: "public" };    }        // Must be authenticated for non-public    if (!user) return null;        // Owner has full access    if (doc.ownerId === user._id) {      return { ...doc, accessLevel: "owner" };    }        // Check shared access    const access = await ctx.db      .query("documentAccess")      .withIndex("by_doc_and_user", (q) =>         q.eq("documentId", args.docId).eq("userId", user._id)      )      .unique();        if (!access) return null;        return { ...doc, accessLevel: access.level };  },});``` ### Action Isolation Audit ```typescript// convex/actions.ts"use node"; import { action, internalAction } from "./_generated/server";import { v } from "convex/values";import { api, internal } from "./_generated/api";import { ConvexError } from "convex/values"; // SECURITY: Never expose API keys in responsesexport const callExternalAPI = action({  args: { query: v.string() },  returns: v.object({ result: v.string() }),  handler: async (ctx, args) => {    // Verify user is authenticated    const identity = await ctx.auth.getUserIdentity();    if (!identity) {      throw new ConvexError("Authentication required");    }        // Get API key from environment (not hardcoded)    const apiKey = process.env.EXTERNAL_API_KEY;    if (!apiKey) {      throw new Error("API key not configured");    }        // Log usage for audit trail    await ctx.runMutation(internal.audit.logAPICall, {      userId: identity.tokenIdentifier,      endpoint: "external-api",      timestamp: Date.now(),    });        const response = await fetch("https://api.example.com/query", {      method: "POST",      headers: {        "Authorization": `Bearer ${apiKey}`,        "Content-Type": "application/json",      },      body: JSON.stringify({ query: args.query }),    });        if (!response.ok) {      // Don't expose external API error details      throw new ConvexError("External service unavailable");    }        const data = await response.json();        // Sanitize response before returning    return { result: sanitizeResponse(data) };  },}); // Internal action - not exposed to clientsexport const _processPayment = internalAction({  args: {    userId: v.id("users"),    amount: v.number(),    paymentMethodId: v.string(),  },  returns: v.object({ success: v.boolean(), transactionId: v.optional(v.string()) }),  handler: async (ctx, args) => {    const stripeKey = process.env.STRIPE_SECRET_KEY;        // Process payment with Stripe    // This should NEVER be exposed as a public action        return { success: true, transactionId: "txn_xxx" };  },});``` ### Rate Limiting Audit ```typescript// convex/rateLimit.tsimport { mutation, query } from "./_generated/server";import { v } from "convex/values";import { ConvexError } from "convex/values"; const RATE_LIMITS = {  message: { requests: 10, windowMs: 60000 }, // 10 per minute  upload: { requests: 5, windowMs: 300000 },  // 5 per 5 minutes  api: { requests: 100, windowMs: 3600000 },  // 100 per hour}; export const checkRateLimit = mutation({  args: {    userId: v.string(),    action: v.union(v.literal("message"), v.literal("upload"), v.literal("api")),  },  returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),  handler: async (ctx, args) => {    const limit = RATE_LIMITS[args.action];    const now = Date.now();    const windowStart = now - limit.windowMs;        // Count requests in window    const requests = await ctx.db      .query("rateLimits")      .withIndex("by_user_and_action", (q) =>         q.eq("userId", args.userId).eq("action", args.action)      )      .filter((q) => q.gt(q.field("timestamp"), windowStart))      .collect();        if (requests.length >= limit.requests) {      const oldestRequest = requests[0];      const retryAfter = oldestRequest.timestamp + limit.windowMs - now;            return { allowed: false, retryAfter };    }        // Record this request    await ctx.db.insert("rateLimits", {      userId: args.userId,      action: args.action,      timestamp: now,    });        return { allowed: true };  },}); // Use in mutationsexport const sendMessage = mutation({  args: { content: v.string() },  returns: v.id("messages"),  handler: async (ctx, args) => {    const identity = await ctx.auth.getUserIdentity();    if (!identity) throw new ConvexError("Authentication required");        // Check rate limit    const rateCheck = await checkRateLimit(ctx, {      userId: identity.tokenIdentifier,      action: "message",    });        if (!rateCheck.allowed) {      throw new ConvexError({        code: "RATE_LIMITED",        message: `Too many requests. Try again in ${Math.ceil(rateCheck.retryAfter! / 1000)} seconds`,      });    }        return await ctx.db.insert("messages", {      content: args.content,      authorId: identity.tokenIdentifier,      createdAt: Date.now(),    });  },});``` ### Sensitive Operations Protection ```typescript// convex/admin.tsimport { mutation, internalMutation } from "./_generated/server";import { v } from "convex/values";import { requireRole, requirePermission } from "./lib/auth";import { internal } from "./_generated/api"; // Two-factor confirmation for dangerous operationsexport const deleteAllUserData = mutation({  args: {    userId: v.id("users"),    confirmationCode: v.string(),  },  returns: v.null(),  handler: async (ctx, args) => {    // Require superadmin    const admin = await requireRole(ctx, "superadmin");        // Verify confirmation code    const confirmation = await ctx.db      .query("confirmations")      .withIndex("by_admin_and_code", (q) =>         q.eq("adminId", admin._id).eq("code", args.confirmationCode)      )      .filter((q) => q.gt(q.field("expiresAt"), Date.now()))      .unique();        if (!confirmation || confirmation.action !== "delete_user_data") {      throw new ConvexError("Invalid or expired confirmation code");    }        // Delete confirmation to prevent reuse    await ctx.db.delete(confirmation._id);        // Schedule deletion (don't do it inline)    await ctx.scheduler.runAfter(0, internal.admin._performDeletion, {      userId: args.userId,      requestedBy: admin._id,    });        // Audit log    await ctx.db.insert("auditLogs", {      action: "delete_user_data",      targetUserId: args.userId,      performedBy: admin._id,      timestamp: Date.now(),    });        return null;  },}); // Generate confirmation code for sensitive actionexport const requestDeletionConfirmation = mutation({  args: { userId: v.id("users") },  returns: v.string(),  handler: async (ctx, args) => {    const admin = await requireRole(ctx, "superadmin");        const code = generateSecureCode();        await ctx.db.insert("confirmations", {      adminId: admin._id,      code,      action: "delete_user_data",      targetUserId: args.userId,      expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes    });        // In production, send code via secure channel (email, SMS)    return code;  },});``` ## Examples ### Complete Audit Trail System ```typescript// convex/audit.tsimport { mutation, query, internalMutation } from "./_generated/server";import { v } from "convex/values";import { getUser, requireRole } from "./lib/auth"; const auditEventValidator = v.object({  _id: v.id("auditLogs"),  _creationTime: v.number(),  action: v.string(),  userId: v.optional(v.string()),  resourceType: v.string(),  resourceId: v.string(),  details: v.optional(v.any()),  ipAddress: v.optional(v.string()),  timestamp: v.number(),}); // Internal: Log audit eventexport const logEvent = internalMutation({  args: {    action: v.string(),    userId: v.optional(v.string()),    resourceType: v.string(),    resourceId: v.string(),    details: v.optional(v.any()),  },  returns: v.id("auditLogs"),  handler: async (ctx, args) => {    return await ctx.db.insert("auditLogs", {      ...args,      timestamp: Date.now(),    });  },}); // Admin: View audit logsexport const getAuditLogs = query({  args: {    resourceType: v.optional(v.string()),    userId: v.optional(v.string()),    limit: v.optional(v.number()),  },  returns: v.array(auditEventValidator),  handler: async (ctx, args) => {    await requireRole(ctx, "admin");        let query = ctx.db.query("auditLogs");        if (args.resourceType) {      query = query.withIndex("by_resource_type", (q) =>         q.eq("resourceType", args.resourceType)      );    }        return await query      .order("desc")      .take(args.limit ?? 100);  },});``` ## Best Practices - Never run `npx convex deploy` unless explicitly instructed- Never run any git commands unless explicitly instructed- Implement defense in depth (multiple security layers)- Log all sensitive operations for audit trails- Use confirmation codes for destructive actions- Rate limit all user-facing endpoints- Never expose internal API keys or errors- Review access patterns regularly ## Common Pitfalls 1. **Single point of failure** - Implement multiple auth checks2. **Missing audit logs** - Log all sensitive operations3. **Trusting client data** - Always validate server-side4. **Exposing error details** - Sanitize error messages5. **No rate limiting** - Always implement rate limits ## References - Convex Documentation: https://docs.convex.dev/- Convex LLMs.txt: https://docs.convex.dev/llms.txt- Functions Auth: https://docs.convex.dev/auth/functions-auth- Production Security: https://docs.convex.dev/production