export type TeamInviteRole = 'admin' | 'member' export interface TeamInviteInput { teamId: string email: string role: TeamInviteRole } export interface TeamAcceptInput { token: string } export class TeamContractError extends Error { constructor( public readonly code: string, public readonly status: number, ) { super(code) this.name = 'TeamContractError' } } const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const INVITE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{20,128}$/ export const CANONICAL_SITE_ORIGIN = 'https://d3ro.chanpaca.net' function requireRecord(value: unknown, code: string): Record { if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new TeamContractError(code, 400) } return value as Record } function hasOnlyKeys(value: Record, allowed: readonly string[]): boolean { const allowlist = new Set(allowed) return Object.keys(value).every((key) => allowlist.has(key)) } export function parseTeamInviteInput(value: unknown): TeamInviteInput { const body = requireRecord(value, 'invalid_invite_request') if (!hasOnlyKeys(body, ['team_id', 'email', 'role'])) { throw new TeamContractError('invalid_invite_request', 400) } const teamId = typeof body.team_id === 'string' ? body.team_id.trim() : '' const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : '' const role = body.role === undefined ? 'member' : body.role if ( !UUID_PATTERN.test(teamId) || email.length < 3 || email.length > 254 || !EMAIL_PATTERN.test(email) || (role !== 'admin' && role !== 'member') ) { throw new TeamContractError('invalid_invite_request', 400) } return { teamId, email, role } } export function parseTeamAcceptInput(value: unknown): TeamAcceptInput { const body = requireRecord(value, 'invalid_invite_token') if (!hasOnlyKeys(body, ['token'])) { throw new TeamContractError('invalid_invite_token', 400) } const token = typeof body.token === 'string' ? body.token.trim() : '' if (!INVITE_TOKEN_PATTERN.test(token)) { throw new TeamContractError('invalid_invite_token', 400) } return { token } } export function normalizeSiteOrigin(value: string | undefined): string { const configured = value?.trim() ?? '' if (!configured) throw new TeamContractError('site_url_not_configured', 503) let url: URL try { url = new URL(configured) } catch { throw new TeamContractError('site_url_invalid', 503) } if ( url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '' ) { throw new TeamContractError('site_url_invalid', 503) } if (url.origin !== CANONICAL_SITE_ORIGIN) { throw new TeamContractError('site_url_invalid', 503) } return url.origin } export function buildInviteUrl(siteOrigin: string, token: string): string { if (!INVITE_TOKEN_PATTERN.test(token)) { throw new TeamContractError('invalid_invite_response', 502) } const url = new URL('/accept-invite/', normalizeSiteOrigin(siteOrigin)) url.searchParams.set('token', token) return url.toString() } export function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } export function buildInviteEmailHtml(input: { inviteUrl: string inviterEmail: string teamName: string role: TeamInviteRole }): string { const inviteUrl = escapeHtml(input.inviteUrl) const inviterEmail = escapeHtml(input.inviterEmail.slice(0, 254)) const teamName = escapeHtml(input.teamName.slice(0, 80) || 'D3RO Voice') const role = input.role === 'admin' ? 'admin' : 'member' return `

D3RO VOICE

Hello.

${inviterEmail} invited you to ${teamName} as a ${role}.

Accept invitation

This invitation expires in 7 days.

${inviteUrl}

` } export function validateEmailSender(value: string): string { const normalized = value.trim() if ( normalized.length < 3 || normalized.length > 320 || /[\r\n]/.test(normalized) ) { throw new TeamContractError('email_configuration_invalid', 503) } return normalized } export function mapTeamRpcError( operation: 'invite' | 'accept', error: { code?: string; message?: string } | null, ): TeamContractError { const message = (error?.message ?? '').toLowerCase() const code = error?.code ?? '' if (operation === 'invite') { if (message.includes('invite_rate_limited') || code === '54000') { return new TeamContractError('invite_rate_limited', 429) } if (message.includes('already_team_member') || code === '23505') { return new TeamContractError('already_team_member', 409) } if (message.includes('team_admin_required') || message.includes('owner_required')) { return new TeamContractError('team_admin_required', 403) } if (message.includes('cannot_invite_self') || message.includes('invalid_invite')) { return new TeamContractError('invalid_invite_request', 400) } if (code === '42883' || message.includes('create_team_invite')) { return new TeamContractError('team_contract_unavailable', 503) } return new TeamContractError('team_invite_failed', 500) } if (message.includes('invite_not_found') || code === 'P0002') { return new TeamContractError('invite_not_found', 404) } if (message.includes('invite_expired')) { return new TeamContractError('invite_expired', 410) } if (message.includes('invite_email_mismatch')) { return new TeamContractError('invite_email_mismatch', 403) } if (message.includes('invite_already_accepted') || code === '23505') { return new TeamContractError('invite_already_accepted', 409) } if (message.includes('invalid_invite_token')) { return new TeamContractError('invalid_invite_token', 400) } if (message.includes('invite_acceptance_conflict') || message.includes('team_membership_failed')) { return new TeamContractError('invite_acceptance_conflict', 409) } if (code === '42883' || message.includes('accept_team_invite')) { return new TeamContractError('team_contract_unavailable', 503) } return new TeamContractError('team_accept_failed', 500) }