koala73/worldmonitor · error · ConvexError
NOT_OWNER
NOT_OWNER
Error message
NOT_OWNER
What it means
`removeSeat` is owner-only: the authenticated user must equal `grant.ownerUserId`. Any other signed-in user (including the invitee themselves) is rejected with NOT_OWNER. This is an authorization guard, not a not-found condition — the grant exists but the caller is not its owner.
Source
Thrown at convex/payments/businessSeats.ts:424
})),
};
},
});
/**
* Owner-only removal of a single seat. Revokes the grant and recomputes the
* invitee's entitlement.
*/
export const removeSeat = mutation({
args: { grantId: v.id("businessProGrants") },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const grant = await ctx.db.get(args.grantId);
if (!grant) {
throw new ConvexError({ kind: "GRANT_NOT_FOUND" });
}
if (grant.ownerUserId !== userId) {
throw new ConvexError({ kind: "NOT_OWNER" });
}
if (grant.status !== "pending" && grant.status !== "accepted") {
return { ok: true as const, status: "already_inactive" as const };
}
const now = Date.now();
// Serialize with inviteSeats via the per-Business-subscription lock row.
await touchBusinessSeatLock(ctx, grant.businessSubscriptionId, now);
await ctx.db.patch(args.grantId, { status: "revoked" });
if (grant.inviteeUserId) {
await ctx.runMutation(
internal.payments.subscriptionHelpers.recomputeEntitlementForUser,
{ userId: grant.inviteeUserId, eventTimestamp: now },
);
}
return { ok: true as const, status: "revoked" as const };View on GitHub (pinned to ffec79ac33)
Solutions
- Only invoke `removeSeat` from the Business subscription owner's authenticated session
- Gate the remove-seat UI on ownership before showing the control
- Use a dedicated invitee self-service/leave flow if one exists, rather than the owner mutation
Defensive patterns
Strategy: validation
Validate before calling
// Only enable the remove-seat control when the current user is the grant's owner.
const isOwner = grant.ownerUserId === currentUserId;
if (!isOwner) { /* hide/disable remove control */ } Type guard
function canRemoveSeat(grant: { ownerUserId: string } | null, currentUserId: string | null): boolean {
return Boolean(grant && currentUserId && grant.ownerUserId === currentUserId);
} Prevention
- Gate the remove-seat UI on `grant.ownerUserId === currentUserId`
- Do not expose removeSeat to invitees; provide a separate self-service flow if needed
When it happens
Trigger: An invitee or an unrelated account calls `removeSeat` on a grant they do not own. The grant resolves successfully, but `grant.ownerUserId !== userId`.
Common situations: An invitee attempts to self-revoke via the owner's endpoint; cross-account confusion where two business owners share an admin surface; frontend bug surfacing the remove control to non-owners.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/5832250e28c71891.
Report an issue: GitHub.