POST /api/sites/{siteId}/invites sends an email to any address the caller names. It is gated on being a site admin and on the site being Pro. It is not throttled — routes/analytics.ts contains no rateLimit at all.
So $19 buys the ability to send unbounded email, from our domain, with our branding, to addresses of the sender's choosing. The recipient sees a legitimate analyticshq invitation because it is one.
Why this is worth fixing before the feature gets traffic
The cost is not the mail bill, it is deliverability. Bulk unsolicited mail from our sending domain gets us onto blocklists, and the mail that then stops arriving is everyone's — password resets, weekly digests, alerts. That is hard to unwind and easy to prevent.
It is also the cheapest possible abuse to automate: one card, one script, an address list.
What already exists to copy
The framework throttles /register — I hit its 429 repeatedly while testing, which is how I noticed invitations have nothing equivalent. route.rateLimit(n, 'minute') is available and is used elsewhere in the org (statushq throttles its invite-accept route with the comment "the uuid is a bearer capability, so throttle guessing").
Suggested
- A per-site and per-account ceiling on invitations sent per hour/day. A real team invites a handful of people; anything above that is either abuse or a bulk-import feature we have not built.
- Throttle
POST /api/invites/accepttoo, on a separate counter. The token is a bearer capability and there is currently nothing slowing a guessing loop, even though the search space makes that impractical rather than impossible. - Consider capping pending invitations per site outright. An account with 400 unaccepted invitations is not using the feature as intended, and that is a cheaper signal to act on than a rate.
Not affected
Redemption itself is sound — hashed token, expiry, single use, bound to the invited address, and never creates an account. This is purely about the sending side being unbounded.