// Models for /admin/* endpoints. Trimmed to what the admin app // currently renders — broaden as we wire more pages. export type WorkerInstallState = | "pending" | "provisioning" | "installed" | "error" | "uninstalling" | "uninstalled"; export type WorkerHealthState = | "healthy" | "watch" | "throttled" | "quarantined" | "blocked"; export interface ManagedWorker { id: string; name: string; notes: string; ip_addr: string; active: boolean; account_count: number; /** Sign-in geography hint the placer scores on. Empty is fine. */ region: string; health_state: WorkerHealthState; load_score: number; ssh_host?: string; ssh_port?: number; ssh_user?: string; ssh_public_key?: string; ssh_host_fingerprint?: string; install_state: WorkerInstallState; last_seen_at?: string; last_error?: string; profile_id?: string; config_applied_at?: string; image_version?: string; tags?: string[]; created_at: string; updated_at: string; } // A mailbox assigned to a worker, with its per-mailbox health signals, returned // by GET /admin/workers/:id/emails. export interface AdminWorkerEmail { id: string; email: string; user_id: string; organization_id?: string | null; status: string; provider: string; warmup_enabled: boolean; last_synced_at: string | null; // null until the mailbox syncs for the first time risk_band: string; // clean | risky | quarantine risk_evaluated_at?: string | null; warmup_health?: string; // worst warmup health_state, "" if not in a pool blocked_until?: string | null; created_at: string; } export interface AdminWorkerEmailsResult { data: AdminWorkerEmail[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface CreateWorkerInput { name: string; notes?: string; /** Optional sign-in geography hint. The only placement input an operator sets. */ region?: string; ssh_host: string; ssh_port?: number; ssh_user?: string; generate_enrollment_token?: boolean; } // The public key and enrollment token are returned once, at creation. Neither is // retrievable later, so the create flow has to surface both before navigating. export interface CreateWorkerResponse extends ManagedWorker { ssh_public_key: string; enrollment_token?: string; enrollment_token_ttl_seconds?: number; } export interface WorkerLiveStatus { service_active: boolean; container_up: boolean; container_image: string; uptime: string; raw: string; } export interface AdminAuditLog { id: string; admin_user_id: string; action: string; target_type: string; target_id: string; details?: Record; ip_address: string; user_agent: string; created_at: string; admin_user?: { id: string; email: string; first_name?: string; last_name?: string; }; } export interface AdminAuditLogSearch { admin_user_id?: string; action?: string; target_type?: string; target_id?: string; start_date?: string; end_date?: string; cursor?: string; limit?: number; } export interface AdminAuditLogsResult { // Nullable on purpose, like AdminUserPreview's slices: a backend older than // the make-not-declare fix in pg_admin.go answers null for an empty page, // and typing it as an array is what let `data.data.length` crash the page. data: AdminAuditLog[] | null; pagination: { next_cursor?: string | null; has_more?: boolean; }; } // /admin/analytics/overview — fields the backend returns vary, so keep // the shape loose. The Overview page renders whatever counters come // back and falls back to "—" for anything missing. export interface PlatformOverview { users_total?: number; users_active_30d?: number; organizations_total?: number; workers_total?: number; workers_active?: number; workers_offline?: number; mailboxes_total?: number; mailboxes_connected?: number; emails_sent_24h?: number; emails_sent_7d?: number; bounce_rate_7d?: number; complaint_rate_7d?: number; campaigns_running?: number; [k: string]: unknown; } // /admin/mailboxes — cross-org mailbox admin. export interface AdminMailboxRow { id: string; email: string; provider: string; status: string; user_id: string; owner_email: string; organization_id?: string | null; org_name?: string | null; worker_id?: string | null; warmup_enabled: boolean; risk_band: string; warmup_pool_type?: string | null; campaign_limit: number; last_synced_at?: string | null; created_at: string; } export interface AdminMailboxesResult { data: AdminMailboxRow[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminMailboxSearch { q?: string; status?: string; provider?: string; warmup?: "on" | "off" | ""; created_within?: number; org_id?: string; // Ownership / placement user_id?: string; worker_id?: string; // Classification risk_band?: string; warmup_pool_type?: string; synced_status?: "never" | "stale" | "recent" | ""; // Flags warmup_paused?: boolean; tracking_domain_verified?: boolean; has_tracking_domain?: boolean; has_organization?: boolean; signature_sync?: boolean; has_oauth?: boolean; has_smtp_imap?: boolean; // Numeric ranges campaign_limit_min?: number; campaign_limit_max?: number; min_wait_time_min?: number; min_wait_time_max?: number; // Date ranges (YYYY-MM-DD) created_after?: string; created_before?: string; last_synced_after?: string; last_synced_before?: string; cursor?: string; limit?: number; sort_by?: "email" | "created_at" | "last_synced_at" | "campaign_limit"; sort_desc?: boolean; } // /admin/analytics — time-series counters for the platform. export interface DailyEmailStat { date: string; total_sent: number; total_delivered: number; total_bounced: number; total_opened: number; total_clicked: number; total_replied: number; } export interface HourlyEmailStat { hour: number; total_sent: number; } export interface UserGrowthStat { date: string; new_users: number; total_users: number; } export interface AnalyticsTrends { users_growth_percent: number; emails_growth_percent: number; campaigns_growth_percent: number; revenue_growth_percent: number; } // /admin/outreach — platform-mailer composer (send from noreply@warmbly.com // with configurable Reply-To, every send audit-logged). export type AdminOutreachStatus = "queued" | "sent" | "failed"; export interface AdminOutreachMessage { id: string; sent_by: string; to_user_id?: string | null; to_org_id?: string | null; to_email: string; reply_to?: string | null; subject: string; body: string; status: AdminOutreachStatus; error?: string | null; sent_at?: string | null; created_at: string; sent_by_user?: AdminUserSummary; to_user?: AdminUserSummary; } export interface SendAdminOutreachRequest { to_user_id?: string; to_org_id?: string; to_email?: string; reply_to?: string; subject: string; body: string; } export interface AdminOutreachSearch { q?: string; status?: AdminOutreachStatus | ""; recipient_type?: "user" | "org" | "email" | ""; sent_by_q?: string; has_reply_to?: boolean; has_error?: boolean; has_user?: boolean; has_org?: boolean; // Date ranges created_within?: number; // days; omit for any created_after?: string; // YYYY-MM-DD created_before?: string; // YYYY-MM-DD sent_at_after?: string; // YYYY-MM-DD sent_at_before?: string; // YYYY-MM-DD cursor?: string; limit?: number; sort_by?: "created_at" | "sent_at" | "status" | "to_email" | "subject"; sort_desc?: boolean; } export interface AdminOutreachResult { data: AdminOutreachMessage[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } // /admin/limit-requests — limit-increase request queue. export type LimitRequestStatus = | "pending" | "approved" | "rejected" | "cancelled"; export interface LimitIncreaseRequest { id: string; organization_id: string; field: string; current_effective: number; requested: number; reason: string; status: LimitRequestStatus; submitted_by: string; submitted_at: string; reviewed_by?: string | null; reviewed_at?: string | null; review_notes: string; organization?: { id: string; name: string; slug?: string | null; }; submitted_by_user?: { id: string; first_name: string; last_name: string; email: string; }; } export interface AdminLimitRequestsResult { data: LimitIncreaseRequest[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminLimitRequestSearch { q?: string; status?: LimitRequestStatus | "all" | ""; field?: string; // one of the 6 app-validated field keys, or "" for any org_id?: string; submitted_by?: string; // Flags reviewed?: boolean; unreviewed?: boolean; // Numeric ranges requested_min?: number; requested_max?: number; current_effective_min?: number; current_effective_max?: number; // Date ranges submitted_within?: number; // days; omit for any submitted_after?: string; // YYYY-MM-DD submitted_before?: string; // YYYY-MM-DD reviewed_after?: string; // YYYY-MM-DD reviewed_before?: string; // YYYY-MM-DD cursor?: string; limit?: number; sort_by?: | "submitted_at" | "requested" | "current_effective" | "reviewed_at" | "status" | "field" | "org_name"; sort_desc?: boolean; } // /admin/campaigns/* — platform-wide campaign admin (force-stop runaway // campaigns, inspect engagement counters per campaign). export interface AdminCampaignDetail { id: string; name: string; user_id: string; organization_id: string; status: string; created_at: string; started_at?: string | null; stopped_at?: string | null; total_contacts: number; emails_sent: number; emails_opened: number; emails_clicked: number; emails_replied: number; emails_bounced: number; user?: AdminUserSummary; organization?: { id: string; name: string; slug?: string | null; }; } export interface AdminCampaignsResult { data: AdminCampaignDetail[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminCampaignSearch { q?: string; user_id?: string; org_id?: string; status?: string; // "" | draft | active | paused | completed | paused_trial_expired | paused_no_accounts | paused_guardrail | paused_undeliverable // Boolean flags open_tracking?: boolean; link_tracking?: boolean; stop_on_reply?: boolean; text_only?: boolean; unsubscribe_header?: boolean; // Relationship existence has_contacts?: boolean; has_bounces?: boolean; // Count ranges daily_limit_min?: number; daily_limit_max?: number; contact_count_min?: number; contact_count_max?: number; sent_count_min?: number; sent_count_max?: number; // Date ranges created_within?: number; created_after?: string; created_before?: string; start_date_after?: string; start_date_before?: string; updated_after?: string; updated_before?: string; // Pagination + sort cursor?: string; limit?: number; sort_by?: | "created_at" | "name" | "status" | "updated_at" | "daily_limit" | "owner_email" | "contact_count" | "sent_count"; sort_desc?: boolean; } // /admin/warmup/* — warmup pool admin (the platform's most critical // safety surface — keeps shared paid pools clean by quarantining // risky mailboxes early). export type WarmupAppealStatus = "pending" | "approved" | "rejected"; export interface WarmupPoolHealthSummary { total_participants: number; by_state: Record; avg_health_score: number; avg_spam_placement_rate: number; spam_placement_by_provider: Record; blocked_count: number; at_risk_count: number; } export interface WarmupPoolInfo { type: string; total_participants: number; active_participants: number; blocked_count: number; } export interface AdminBlockedAccount { id: string; email: string; user_id: string; blocked_at: string; blocked_by?: string | null; block_reason: string; has_appeal: boolean; appeal_status?: WarmupAppealStatus | null; user?: AdminUserSummary; } export interface WarmupAppeal { id: string; email_account_id: string; user_id: string; reason: string; status: WarmupAppealStatus; reviewed_by?: string | null; reviewed_at?: string | null; review_notes?: string | null; created_at: string; user?: AdminUserSummary; email_account?: { id: string; email: string; }; reviewed_by_user?: AdminUserSummary; } export interface WarmupAppealsResult { data: WarmupAppeal[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminBlockedAccountsResult { data: AdminBlockedAccount[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface BlockAccountRequest { reason: string; } export interface ReviewAppealRequest { approved: boolean; notes: string; } // /admin/users/* — platform user admin. export interface AdminUserSummary { id: string; first_name: string; last_name: string; email: string; } export interface AdminUserDetail { id: string; first_name: string; last_name: string; email: string; max_organizations: number; free_trial_used: boolean; admin_permissions: number; admin_granted_at?: string | null; admin_granted_by?: string | null; banned_at?: string | null; created_at: string; updated_at: string; organization_count: number; email_account_count: number; campaign_count: number; } export interface AdminUsersResult { data: AdminUserDetail[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminUserSearchParams { q?: string; status?: "active" | "banned" | "all" | ""; is_admin?: boolean; has_overrides?: boolean; free_trial_used?: boolean; created_within?: number; // Plan / subscription plan_id?: string; subscription_status?: string; is_enterprise?: boolean; has_subscription?: boolean; has_active_subscription?: boolean; // Account state onboarding_completed?: boolean; deletion_scheduled?: boolean; has_avatar?: boolean; has_active_campaign?: boolean; has_ban_record?: boolean; has_dedicated_worker?: boolean; // Count ranges org_count_min?: number; org_count_max?: number; email_account_count_min?: number; email_account_count_max?: number; campaign_count_min?: number; campaign_count_max?: number; max_organizations_min?: number; max_organizations_max?: number; // Date ranges (YYYY-MM-DD) created_after?: string; created_before?: string; admin_granted_after?: string; admin_granted_before?: string; banned_after?: string; banned_before?: string; updated_after?: string; updated_before?: string; cursor?: string; limit?: number; sort_by?: "created_at" | "email" | "name"; sort_desc?: boolean; } export interface UserBan { id: string; user_id: string; banned_by: string; reason: string; banned_at: string; unbanned_at?: string | null; unbanned_by?: string | null; unban_reason?: string | null; banned_by_user?: AdminUserSummary; unbanned_by_user?: AdminUserSummary | null; } // One user's row in user_rate_limits: the API and realtime throughput they are // allowed. Every value is set; a user with no row reads back the defaults. export interface AdminUserRateLimits { user_id: string; limit_read_pm: number; limit_write_pm: number; limit_bulk_pm: number; limit_unibox_pm: number; limit_analytics_pm: number; limit_api_calls_daily: number; limit_bulk_ops_daily: number; limit_ws_message_pm: number; limit_ws_join_pm: number; limit_ws_event_pm: number; max_connections: number; notes?: string | null; updated_by?: string | null; updated_at?: string | null; // absent when these are the defaults } export interface UpdateUserRateLimitsRequest { limit_read_pm?: number; limit_write_pm?: number; limit_bulk_pm?: number; limit_unibox_pm?: number; limit_analytics_pm?: number; limit_api_calls_daily?: number; limit_bulk_ops_daily?: number; limit_ws_message_pm?: number; limit_ws_join_pm?: number; limit_ws_event_pm?: number; max_connections?: number; notes?: string; } export interface BanUserRequest { reason: string; // Bitmask of ban-scope flags. 1 = login, 2 = org-create, 4 = send. // 0 falls back to "login only" server-side. See // internal/models/admin.go BanScope. scope?: number; } export interface UnbanUserRequest { reason: string; } // The four slices are nullable on purpose: a backend older than the // empty-slice fix in pg_admin.go answers null, and typing them as arrays is // what let `email_accounts.length` take the whole page down. export interface AdminUserPreview { user: AdminUserDetail; organizations: Array<{ id: string; name: string; slug?: string | null; owner_user_id: string; created_at: string; updated_at: string; }> | null; subscriptions: Array<{ id: string; organization_id: string; plan_id: string; status: string; is_enterprise: boolean; current_period_end?: string | null; trial_end?: string | null; }> | null; email_accounts: Array<{ id: string; email: string; organization_id?: string | null; status: string; provider: string; warmup_enabled: boolean; last_synced_at: string; }> | null; recent_bans: UserBan[] | null; rate_limits?: AdminUserRateLimits | null; } // /admin/organizations* — workspace admin (read-only slice). export interface AdminOrgListItem { category: "standard" | "test"; id: string; name: string; slug?: string | null; owner_user_id: string; owner_email: string; owner_first_name: string; owner_last_name: string; owner_banned_at?: string | null; created_at: string; deletion_scheduled_for?: string | null; member_count: number; email_account_count: number; campaign_count: number; active_campaigns: number; risk_state?: OrgRiskState | null; /** Acquisition channel recorded once at signup. Absent for a direct * signup, which is most of them, and always absent on a self-host. */ utm_source?: string | null; utm_medium?: string | null; utm_campaign?: string | null; landing_path?: string | null; plan_name?: string | null; plan_public?: boolean | null; is_enterprise: boolean; /** Paid because an operator said so, not because Stripe says so. */ managed_plan: boolean; /** A grant that lapsed. Distinct from never having had one: the workspace * is back on free and the reason is still on file. */ managed_plan_expired: boolean; managed_plan_reason?: string | null; managed_plan_until?: string | null; } /** The workspace's fused abuse posture. */ export type OrgRiskState = "trusted" | "watch" | "restricted" | "suspended"; /** One detector's finding. `expires_at` is absent when only a sweep or an * operator can retract it. */ export interface OrgRiskSignal { weight?: number; detail?: string; expires_at?: string; } /** An operator's pin over the derived posture. */ export interface OrgRiskOverride { state: OrgRiskState; reason?: string; by?: string | null; at?: string | null; } /** The operator view of a workspace's posture, evidence included. */ export interface OrgRisk { organization_id: string; state: OrgRiskState; score: number; reason?: string; signals?: Record | null; evaluated_at?: string | null; override?: OrgRiskOverride | null; } export interface SetOrgRiskOverrideRequest { state: OrgRiskState; reason: string; } export interface OrganizationLimits { max_campaigns?: number | null; max_active_campaigns?: number | null; max_team_members?: number | null; max_email_accounts?: number | null; max_contacts?: number | null; daily_campaign_limit?: number | null; } export interface OrganizationCounts { total_campaigns: number; active_campaigns: number; total_contacts: number; total_members: number; email_accounts: number; emails_sent_today: number; } export interface OrganizationLimitOverrides { organization_id: string; max_campaigns: number; max_active_campaigns: number; max_team_members: number; max_email_accounts: number; max_contacts: number; daily_campaign_limit: number; granted_by?: string | null; granted_at: string; updated_at: string; notes: string; } export interface UpdateOrgOverridesRequest { max_campaigns?: number; max_active_campaigns?: number; max_team_members?: number; max_email_accounts?: number; max_contacts?: number; daily_campaign_limit?: number; notes?: string; } export interface AdminOrgDetail extends AdminOrgListItem { updated_at: string; deletion_scheduled_at?: string | null; limits?: OrganizationLimits | null; overrides?: OrganizationLimitOverrides | null; effective_limits?: OrganizationLimits | null; counts?: OrganizationCounts | null; plan_name?: string | null; subscription_status?: string | null; is_enterprise: boolean; current_period_end?: string | null; trial_end?: string | null; } export interface AdminOrgsResult { data: AdminOrgListItem[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminOrgMember { id: string; organization_id: string; user_id: string; role: string; permissions: number; invited_by?: string | null; invited_at: string; accepted_at?: string | null; user?: { id: string; first_name: string; last_name: string; email: string; }; } export interface AdminOrgMembersResult { data: AdminOrgMember[]; } export interface AdminOrgSearch { q?: string; status?: "active" | "pending_deletion" | ""; plan_id?: string; plan_visibility?: "public" | "private" | "none" | ""; created_within?: number; // days; omit for any has_overrides?: boolean; enterprise?: boolean; /** Plans an operator granted rather than Stripe. */ managed_plan?: boolean; risk_state?: OrgRiskState | ""; risk_flagged?: boolean; // Acquisition channel utm_source?: string; utm_medium?: string; utm_campaign?: string; has_acquisition?: boolean; no_acquisition?: boolean; // Subscription state subscription_status?: string; cancel_at_period_end?: boolean; has_active_subscription?: boolean; no_subscription?: boolean; owner_banned?: boolean; // Relationship existence has_active_campaigns?: boolean; has_email_accounts?: boolean; // Count ranges member_count_min?: number; member_count_max?: number; email_account_count_min?: number; email_account_count_max?: number; campaign_count_min?: number; campaign_count_max?: number; // Date ranges (YYYY-MM-DD) created_after?: string; created_before?: string; trial_end_after?: string; trial_end_before?: string; current_period_end_after?: string; current_period_end_before?: string; updated_after?: string; updated_before?: string; cursor?: string; limit?: number; sort_by?: "created_at" | "name" | "owner_email" | "member_count" | "email_account_count" | "campaign_count"; sort_desc?: boolean; } /** A plan an operator granted rather than Stripe, for internal workspaces, * design partners and support gestures. */ export interface ManagedPlan { managed: boolean; expired: boolean; plan_id: string; granted_at?: string | null; granted_by?: string | null; reason?: string | null; until?: string | null; } /** A plan row as the admin plans endpoint returns it. Only the fields the * grant picker needs. */ export interface AdminPlan { id: string; name?: string | null; public?: boolean | null; price?: number | string | null; } /** An account excused from the emailed login code. */ export interface LoginCodeExemption { user_id: string; email: string; reason?: string | null; granted_at?: string | null; /** When the handed-out password stops working; absent for an account * exempted without one. */ password_expires_at?: string | null; } /** The one time the password is readable. It is not stored in a form anyone * can recover, so losing it means making another tester. */ export interface CreatedTester { user_id: string; email: string; organization_id: string; password: string; /** When the password stops working. */ password_expires_at: string; /** True when the tester joined a workspace that already existed rather * than one minted for it. */ joined_existing: boolean; } /** A workspace role, as the Testers page offers them. Roles are ordinary rows * an org can rename or delete, so the list is per workspace and not fixed. */ export interface AdminOrgRole { id: string; organization_id: string; name: string; description?: string | null; color?: string | null; permissions: number; } // --- Promo codes ----------------------------------------------------------- // // Mirrors internal/models/discount.go. A code is the operator-owned half of // the discount system: the customer types it at checkout, the backend // validates it here and mints a matching one-off Stripe coupon per redemption. export type DiscountType = "percent" | "fixed" | "trial_extension"; export type DiscountDuration = "once" | "repeating" | "forever"; export type DiscountCodeStatus = "active" | "disabled" | "expired"; export type DiscountRedemptionStatus = "pending" | "applied" | "canceled"; export interface DiscountCode { id: string; code: string; description: string; type: DiscountType; percent_off?: number | null; amount_off?: number | null; currency?: string | null; trial_extension_days?: number | null; duration: DiscountDuration; duration_in_months?: number | null; /** null = uncapped across all workspaces. */ max_redemptions?: number | null; times_redeemed: number; /** How many times ONE workspace may redeem it. Always at least 1. */ per_account_limit: number; applies_to_all_plans: boolean; plan_ids: string[]; status: DiscountCodeStatus; starts_at?: string | null; expires_at?: string | null; created_by?: string | null; created_at: string; updated_at: string; } /** One workspace's use of a code. The value fields are a snapshot taken at * redemption, so editing the code later never rewrites history. */ export interface DiscountRedemption { id: string; discount_code_id: string; organization_id: string; redeemed_by?: string | null; subscription_id?: string | null; plan_id?: string | null; stripe_coupon_id?: string | null; stripe_checkout_session_id?: string | null; type: DiscountType; percent_off?: number | null; amount_off?: number | null; currency?: string | null; trial_extension_days?: number | null; status: DiscountRedemptionStatus; redeemed_at: string; applied_at?: string | null; code?: string; } export interface AdminDiscountsResult { data: DiscountCode[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminDiscountRedemptionsResult { data: DiscountRedemption[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminDiscountSearch { /** The backend binds free text as `search`, not `q`. */ search?: string; status?: DiscountCodeStatus | "all" | ""; type?: DiscountType | ""; duration?: DiscountDuration | ""; plan_scope?: "all" | "specific" | ""; plan_id?: string; has_redemptions?: boolean; has_max_redemptions?: boolean; exhausted?: boolean; has_expiry?: boolean; times_redeemed_min?: number; times_redeemed_max?: number; percent_off_min?: number; percent_off_max?: number; created_within?: number; created_after?: string; created_before?: string; starts_after?: string; starts_before?: string; expires_after?: string; expires_before?: string; cursor?: string; limit?: number; sort_by?: | "created_at" | "code" | "status" | "times_redeemed" | "expires_at" | "starts_at" | "updated_at"; sort_desc?: boolean; } export interface CreateDiscountCodeRequest { code: string; description?: string; type: DiscountType; percent_off?: number; amount_off?: number; currency?: string; trial_extension_days?: number; duration?: DiscountDuration; duration_in_months?: number; max_redemptions?: number; per_account_limit?: number; applies_to_all_plans: boolean; plan_ids?: string[]; status?: DiscountCodeStatus; starts_at?: string; expires_at?: string; } /** Partial update. `type` is immutable; recreate the code to change kinds. * The three caps take an explicit null to clear them: an omitted key leaves * the column alone, which is not the same as lifting an expiry. */ export type UpdateDiscountCodeRequest = Partial< Omit > & { max_redemptions?: number | null; starts_at?: string | null; expires_at?: string | null; }; // Community app directory (/admin/app-listings). published = link only (listed // once enough workspaces use it), featured = picked by us, hidden = taken down. export type AppListingStatus = "published" | "featured" | "hidden"; export interface AdminAppListing { application_id: string; organization_id: string; slug: string; tagline: string; description: string; category: string; install_url: string; support_url: string; privacy_url: string; status: AppListingStatus; status_note?: string; status_at?: string | null; submitted_at: string; created_at: string; updated_at: string; name: string; logo_url: string; website_url: string; scopes: number; permissions: { name: string; value: number; description: string; category: string }[]; app_status: string; organization_name: string; installs: number; /** Shown in the directory: featured, or used by enough workspaces. */ listed: boolean; status_by?: string | null; status_by_email?: string; } export interface AdminAppListingsResult { data: AdminAppListing[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean; }; } export interface AdminAppListingSearch { q?: string; status?: AppListingStatus | ""; limit?: number; cursor?: string; } // OAuth app moderation (/admin/oauth-apps) and developer blocks. export interface AdminOAuthApp { id: string; organization_id: string; organization_name: string; created_by: string; created_by_email: string; name: string; description: string; logo_url: string; website_url: string; client_id: string; redirect_uris: string[]; webhook_url: string; permissions: { name: string; value: number; description: string; category: string }[]; status: "active" | "disabled"; is_public: boolean; suspended_at?: string | null; suspended_reason?: string; installs: number; listing_slug?: string; listing_status?: AppListingStatus | ""; org_blocked: boolean; creator_blocked: boolean; created_at: string; } export interface AdminOAuthAppsResult { data: AdminOAuthApp[]; pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean }; } export interface AdminOAuthAppSearch { q?: string; status?: "active" | "disabled" | "suspended" | ""; limit?: number; cursor?: string; } export interface OAuthDeveloperBlock { id: string; organization_id?: string | null; organization_name?: string; user_id?: string | null; user_email?: string; reason: string; blocked_by_email?: string; created_at: string; }