{"record":{"id":"5832250e28c71891","repo":"koala73/worldmonitor","slug":"not-owner","errorCode":"NOT_OWNER","errorMessage":"NOT_OWNER","messagePattern":"NOT_OWNER","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":424,"sourceCode":"      })),\n    };\n  },\n});\n\n/**\n * Owner-only removal of a single seat. Revokes the grant and recomputes the\n * invitee's entitlement.\n */\nexport const removeSeat = mutation({\n  args: { grantId: v.id(\"businessProGrants\") },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const grant = await ctx.db.get(args.grantId);\n    if (!grant) {\n      throw new ConvexError({ kind: \"GRANT_NOT_FOUND\" });\n    }\n    if (grant.ownerUserId !== userId) {\n      throw new ConvexError({ kind: \"NOT_OWNER\" });\n    }\n    if (grant.status !== \"pending\" && grant.status !== \"accepted\") {\n      return { ok: true as const, status: \"already_inactive\" as const };\n    }\n\n    const now = Date.now();\n    // Serialize with inviteSeats via the per-Business-subscription lock row.\n    await touchBusinessSeatLock(ctx, grant.businessSubscriptionId, now);\n\n    await ctx.db.patch(args.grantId, { status: \"revoked\" });\n\n    if (grant.inviteeUserId) {\n      await ctx.runMutation(\n        internal.payments.subscriptionHelpers.recomputeEntitlementForUser,\n        { userId: grant.inviteeUserId, eventTimestamp: now },\n      );\n    }\n    return { ok: true as const, status: \"revoked\" as const };","sourceCodeStart":406,"sourceCodeEnd":442,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L406-L442","documentation":"`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.","triggerScenarios":"An invitee or an unrelated account calls `removeSeat` on a grant they do not own. The grant resolves successfully, but `grant.ownerUserId !== userId`.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Only enable the remove-seat control when the current user is the grant's owner.\nconst isOwner = grant.ownerUserId === currentUserId;\nif (!isOwner) { /* hide/disable remove control */ }","typeGuard":"function canRemoveSeat(grant: { ownerUserId: string } | null, currentUserId: string | null): boolean {\n  return Boolean(grant && currentUserId && grant.ownerUserId === currentUserId);\n}","tryCatchPattern":null,"preventionTips":["Gate the remove-seat UI on `grant.ownerUserId === currentUserId`","Do not expose removeSeat to invitees; provide a separate self-service flow if needed"],"tags":["payments","business-seats","authorization","convex"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}