// Invite a teammate — spends a `seats` entitlement. Same shape as // projects.create: the billing plugin merges `EntitlementExceeded` into the // wire-error union, so running out of seats surfaces typed and the frontend // shows "buy more seats" (billing.changeSeats) instead of a generic error. import { defineMutation } from '@voltro/protocol' import { Schema } from 'effect' export const createInvite = defineMutation({ name: 'invites.create', target: { table: 'invites', op: 'insert' }, // Members only, and on its OWN scope rather than `projects:write` — the // vocabulary is split by blast radius, not by convenience. Inviting a person // spends a seat (real money) and puts an outsider inside the tenant; creating // a project does neither. When this app grows an `owner` role, `team:invite` // is the scope that moves to it and `projects:write` is the one that stays. // // The guard runs before `requireEntitlement`, so an anonymous call cannot // spend the tenant's seat quota. Satisfiable as shipped: a matched session // gets `team:invite` from `auth.resolveScopes` (authz.ts). guards: [{ scope: 'team:invite' }], input: Schema.Struct({ email: Schema.NonEmptyString }), output: Schema.Struct({ id: Schema.String, email: Schema.String, status: Schema.String, tenantId: Schema.String, }), })