PR: #342: [Story-1350] D2 front door: reject internal-domain emails in firm member invite
by @ankitmaheshwari-com
View PR →
Story 1350
D2 front door: reject internal-domain invites
Per pramaan-docs ADR-123, a firm admin must not be able
to invite a PRAMAAN employee into their firm. This closes the front
door so an internal-domain email (default pramaan.io) is
rejected before a Clerk organization invitation is ever created. The
pramaan-functions Story-1347 webhook remains the backend backstop.
Shared, configurable domain list
-
New
src/lib/team-invitations/internal-domains.tsis the single source of truth:internalEmailDomains()andisInternalEmailDomain(email). -
Configurable via
NEXT_PUBLIC_PRAMAAN_INTERNAL_EMAIL_DOMAINS(comma-separated), defaultpramaan.io. Nothing is hard-coded inline at the call sites. -
The
NEXT_PUBLIC_prefix is deliberate: the same list resolves on the client (form validation) and on the server (the Clerk-issuing action). Parsing is trimmed, lowercased, and blank-safe.
Front-door enforcement (two layers)
-
Client / form.
teamInvitationCreateRequestSchemagains an email.refinethat flags internal domains with a machine-readable marker (INTERNAL_DOMAIN_ISSUE). Because this schema also backs the/api/internal/team-invitationsroute'ssafeParse, the API boundary rejects internal domains too. -
Server action.
createTeamInvitationthrows aTeamInvitationAccessError(422,pramaan.team_invitations.internal_domain_blocked) before callingclerkClient().organizations.createOrganizationInvitation— the definitive front door.
Clear inline message, localized
-
invite-member-fields.tsxmaps the marker to a distinct inline error (Team.invite.emailInternalDomainError) with the configured domain interpolated, separate from the generic "enter a valid email" copy. -
use-team-list-actions.tsmaps the server error code to a form-level message (Team.invite.internalDomainBlocked) for any path that bypasses the form (e.g. client/server config divergence). -
Both new keys exist in
messages/en.jsonandmessages/hi.json.
Acceptance criteria
-
Inviting
someone@pramaan.iois rejected client-side with a clear message; no Clerk org invitation is created. met -
Domain list is env/shared-config driven, default
pramaan.io, not hard-coded. met - Non-internal-domain invites work unchanged. met
- Cites
pramaan-docs ADR-123; references Story-1347.
Verification
-
pnpm lintpassed (only pre-existing add-matter warnings). -
pnpm test528 passed, including the newinternal-domains.test.tsand the server-guard case inserver.test.ts. pnpm buildpassed.git diff --checkclean.
Files changed
9 file(s), +168 / -3 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +2 | -0 |
| messages/hi.json | Config | +2 | -0 |
| src/components/settings/team/invite-member-fields.tsx | Component | +15 | -2 |
| src/components/settings/team/use-team-list-actions.ts | Component | +6 | -0 |
| src/lib/team-invitations/internal-domains.test.ts | Library | +61 | -0 |
| src/lib/team-invitations/internal-domains.ts | Library | +43 | -0 |
| src/lib/team-invitations/schema.ts | Library | +13 | -1 |
| src/lib/team-invitations/server.test.ts | Library | +15 | -0 |
| src/lib/team-invitations/server.ts | Library | +11 | -0 |