convex-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Security

Convex 安全实践

Error Handling with ConvexError

使用ConvexError处理错误

Use
ConvexError
for user-facing errors with structured error codes:
typescript
import { ConvexError } from "convex/values";

export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const task = await ctx.db.get("tasks", args.taskId);

    if (!task) {
      throw new ConvexError({
        code: "NOT_FOUND",
        message: "Task not found",
      });
    }

    await ctx.db.patch("tasks", args.taskId, { title: args.title });
    return null;
  },
});
Common error codes:
UNAUTHENTICATED
,
FORBIDDEN
,
NOT_FOUND
,
RATE_LIMITED
,
VALIDATION_ERROR
针对面向用户的错误,使用
ConvexError
并配合结构化错误码:
typescript
import { ConvexError } from "convex/values";

export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const task = await ctx.db.get("tasks", args.taskId);

    if (!task) {
      throw new ConvexError({
        code: "NOT_FOUND",
        message: "Task not found",
      });
    }

    await ctx.db.patch("tasks", args.taskId, { title: args.title });
    return null;
  },
});
常见错误码:
UNAUTHENTICATED
,
FORBIDDEN
,
NOT_FOUND
,
RATE_LIMITED
,
VALIDATION_ERROR

Argument AND Return Validators

参数与返回值验证器

Always define both
args
and
returns
validators:
typescript
export const createTask = mutation({
  args: {
    title: v.string(),
    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
  },
  returns: v.id("tasks"),  // Always include returns!
  handler: async (ctx, args) => {
    return await ctx.db.insert("tasks", {
      title: args.title,
      priority: args.priority,
      completed: false,
    });
  },
});
ESLint: Use
@convex-dev/require-argument-validators
rule.
务必同时定义
args
returns
验证器:
typescript
export const createTask = mutation({
  args: {
    title: v.string(),
    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
  },
  returns: v.id("tasks"),  // Always include returns!
  handler: async (ctx, args) => {
    return await ctx.db.insert("tasks", {
      title: args.title,
      priority: args.priority,
      completed: false,
    });
  },
});
ESLint 规则: 使用
@convex-dev/require-argument-validators
规则。

Authentication Helpers

认证助手工具

Create reusable auth helpers:
typescript
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";

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 requireAuth(ctx: QueryCtx | MutationCtx): Promise<Doc<"users">> {
  const user = await getUser(ctx);
  if (!user) {
    throw new ConvexError({
      code: "UNAUTHENTICATED",
      message: "Authentication required",
    });
  }
  return user;
}

type UserRole = "user" | "moderator" | "admin";
const roleHierarchy: Record<UserRole, number> = { user: 0, moderator: 1, admin: 2 };

export async function requireRole(
  ctx: QueryCtx | MutationCtx,
  minRole: UserRole
): Promise<Doc<"users">> {
  const user = await requireAuth(ctx);
  const userLevel = roleHierarchy[user.role as UserRole] ?? 0;

  if (userLevel < roleHierarchy[minRole]) {
    throw new ConvexError({
      code: "FORBIDDEN",
      message: `Role '${minRole}' or higher required`,
    });
  }
  return user;
}
Usage:
typescript
export const adminOnly = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await requireRole(ctx, "admin");
    // ... admin logic
    return null;
  },
});
创建可复用的认证助手:
typescript
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";

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 requireAuth(ctx: QueryCtx | MutationCtx): Promise<Doc<"users">> {
  const user = await getUser(ctx);
  if (!user) {
    throw new ConvexError({
      code: "UNAUTHENTICATED",
      message: "Authentication required",
    });
  }
  return user;
}

type UserRole = "user" | "moderator" | "admin";
const roleHierarchy: Record<UserRole, number> = { user: 0, moderator: 1, admin: 2 };

export async function requireRole(
  ctx: QueryCtx | MutationCtx,
  minRole: UserRole
): Promise<Doc<"users">> {
  const user = await requireAuth(ctx);
  const userLevel = roleHierarchy[user.role as UserRole] ?? 0;

  if (userLevel < roleHierarchy[minRole]) {
    throw new ConvexError({
      code: "FORBIDDEN",
      message: `Role '${minRole}' or higher required`,
    });
  }
  return user;
}
使用示例:
typescript
export const adminOnly = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await requireRole(ctx, "admin");
    // ... admin logic
    return null;
  },
});

Row-Level Access Control

行级访问控制

Verify ownership before operations:
typescript
export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireAuth(ctx);
    const task = await ctx.db.get("tasks", args.taskId);

    if (!task || task.userId !== user._id) {
      throw new ConvexError({ code: "FORBIDDEN", message: "Not authorized" });
    }

    await ctx.db.patch("tasks", args.taskId, { title: args.title });
    return null;
  },
});
在执行操作前验证所有权:
typescript
export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireAuth(ctx);
    const task = await ctx.db.get("tasks", args.taskId);

    if (!task || task.userId !== user._id) {
      throw new ConvexError({ code: "FORBIDDEN", message: "Not authorized" });
    }

    await ctx.db.patch("tasks", args.taskId, { title: args.title });
    return null;
  },
});

Rate Limiting

速率限制

Prevent abuse with rate limiting:
typescript
const RATE_LIMITS = {
  message: { requests: 10, windowMs: 60000 },  // 10 per minute
  upload: { requests: 5, windowMs: 300000 },   // 5 per 5 minutes
};

export const checkRateLimit = mutation({
  args: { userId: v.string(), action: v.string() },
  returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),
  handler: async (ctx, args) => {
    const limit = RATE_LIMITS[args.action];
    const windowStart = Date.now() - limit.windowMs;

    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) {
      return { allowed: false, retryAfter: requests[0].timestamp + limit.windowMs - Date.now() };
    }

    await ctx.db.insert("rateLimits", {
      userId: args.userId,
      action: args.action,
      timestamp: Date.now(),
    });
    return { allowed: true };
  },
});
通过速率限制防止滥用:
typescript
const RATE_LIMITS = {
  message: { requests: 10, windowMs: 60000 },  // 10 per minute
  upload: { requests: 5, windowMs: 300000 },   // 5 per 5 minutes
};

export const checkRateLimit = mutation({
  args: { userId: v.string(), action: v.string() },
  returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),
  handler: async (ctx, args) => {
    const limit = RATE_LIMITS[args.action];
    const windowStart = Date.now() - limit.windowMs;

    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) {
      return { allowed: false, retryAfter: requests[0].timestamp + limit.windowMs - Date.now() };
    }

    await ctx.db.insert("rateLimits", {
      userId: args.userId,
      action: args.action,
      timestamp: Date.now(),
    });
    return { allowed: true };
  },
});

Internal Functions Only for Scheduling

仅使用内部函数进行调度

Always use
internal.*
for
ctx.run*
and scheduling:
typescript
// Bad - exposes public function
await ctx.scheduler.runAfter(0, api.tasks.process, { id });

// Good - uses internal function
await ctx.scheduler.runAfter(0, internal.tasks.process, { id });

// Internal function definition
export const process = internalMutation({
  args: { id: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ... processing logic
    return null;
  },
});
Tip: Never import
api
from
_generated/api.ts
in Convex functions.
在使用
ctx.run*
和调度任务时,务必使用
internal.*
函数:
typescript
// Bad - exposes public function
await ctx.scheduler.runAfter(0, api.tasks.process, { id });

// Good - uses internal function
await ctx.scheduler.runAfter(0, internal.tasks.process, { id });

// Internal function definition
export const process = internalMutation({
  args: { id: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ... processing logic
    return null;
  },
});
提示: 不要在Convex函数中从
_generated/api.ts
导入
api

Include Table Name in ctx.db

在ctx.db中明确指定表名

Always include table name as first argument:
typescript
// Bad
await ctx.db.get(movieId);
await ctx.db.patch(movieId, { title: "Whiplash" });

// Good
await ctx.db.get("movies", movieId);
await ctx.db.patch("movies", movieId, { title: "Whiplash" });
ESLint: Use
@convex-dev/explicit-table-ids
rule with autofix.
务必将表名作为第一个参数传入:
typescript
// Bad
await ctx.db.get(movieId);
await ctx.db.patch(movieId, { title: "Whiplash" });

// Good
await ctx.db.get("movies", movieId);
await ctx.db.patch("movies", movieId, { title: "Whiplash" });
ESLint规则: 启用
@convex-dev/explicit-table-ids
规则并使用自动修复功能。

References

参考资料