Agent Wranglers

Organisations and teams Required

Make invitations expire and work once

Send each invitation as a single-use link that expires within 7 days, with one role chosen at sending. Let admins see and cancel pending invitations.

How to do it

Make the token at least 128 random bits and store only its hash, with the organisation, invited email key, role and inviter. An inviter may grant only roles up to their own. Accept only from a signed-in account whose verified email matches, unless an admin allowed any address. Show the organisation and the inviter's name before acceptance. Allow at most 50 invitations a day per organisation, to stop spam. Cancel pending invitations when the inviter loses the right to invite.

Example

const INVITATION_DAYS = 7;
const DAY_MS = 24 * 60 * 60 * 1000;

export async function acceptInvitation(db, user, token) {
  const invite = await db.invitations.findUnusedByHash(sha256(token));
  if (!invite || Date.now() - invite.createdAt > INVITATION_DAYS * DAY_MS) throw new GoneError('This invitation has expired.');
  if (!user.emailVerifiedAt || user.emailKey !== invite.emailKey) throw new ForbiddenError('This invitation is for another address.');
  await db.tx(async tx => {
    await tx.invitations.markUsed(invite.id);
    await tx.memberships.add(invite.organisationId, user.id, invite.role);
  });
}
INVITATION_LIFETIME = timedelta(days=7)

def accept_invitation(db, user, token: str) -> None:
    invite = db.invitations.find_unused_by_hash(sha256(token))
    if invite is None or datetime.now(UTC) - invite.created_at > INVITATION_LIFETIME:
        raise Gone("This invitation has expired.")
    if user.email_verified_at is None or user.email_key != invite.email_key:
        raise Forbidden("This invitation is for another address.")
    with db.transaction():
        db.invitations.mark_used(invite.id)
        db.memberships.add(invite.organisation_id, user.id, invite.role)

How to check

Accept an invitation, then open it again, and it fails. An 8-day-old invitation fails. Accepting while signed in with another email fails. A member inviting someone as owner fails.

For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers accounts playbook: https://agentwranglers.se/playbook/accounts/organisations-and-teams/invitations.