diff --git a/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx b/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx
index 828df1cb8d..f9dc5f5fe3 100644
--- a/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx
+++ b/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx
@@ -7,7 +7,7 @@ import { NotificationBell } from '@/components/notifications/notification-bell';
import { OrganizationSwitcher } from '@/components/organization-switcher';
import { SidebarProvider, useSidebar } from '@/context/sidebar-context';
import { authClient } from '@/utils/auth-client';
-import { CertificateCheck, CloudAuditing, Logout, Security, Settings } from '@carbon/icons-react';
+import { Badge, Globe, Logout, ManageProtection, Settings } from '@carbon/icons-react';
import {
DropdownMenu,
DropdownMenuContent,
@@ -240,14 +240,14 @@ function AppShellWrapperContent({
}
+ icon={
}
label="Compliance"
/>
{isTrustNdaEnabled && (
}
+ icon={
}
label="Trust"
/>
)}
@@ -255,7 +255,7 @@ function AppShellWrapperContent({
}
+ icon={
}
label="Security"
/>
) : null}
@@ -282,7 +282,7 @@ function AppShellWrapperContent({
}
/>
{isSettingsActive ? (
-
+
) : isTrustActive ? (
) : isSecurityActive && isSecurityEnabled ? (
diff --git a/apps/app/src/app/(app)/[orgId]/documents/components/CompanyFormPageClient.tsx b/apps/app/src/app/(app)/[orgId]/documents/components/CompanyFormPageClient.tsx
index da14356e33..380e05a88e 100644
--- a/apps/app/src/app/(app)/[orgId]/documents/components/CompanyFormPageClient.tsx
+++ b/apps/app/src/app/(app)/[orgId]/documents/components/CompanyFormPageClient.tsx
@@ -10,8 +10,19 @@ import { api } from '@/lib/api-client';
import { useActiveMember } from '@/utils/auth-client';
import { jwtManager } from '@/utils/jwt-manager';
import {
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
Badge,
Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
Empty,
EmptyDescription,
EmptyHeader,
@@ -29,7 +40,15 @@ import {
TableRow,
Text,
} from '@trycompai/design-system';
-import { Add, Catalog, Download, Search, Upload } from '@trycompai/design-system/icons';
+import {
+ Add,
+ Catalog,
+ Download,
+ OverflowMenuVertical,
+ Search,
+ TrashCan,
+ Upload,
+} from '@trycompai/design-system/icons';
import {
Dialog,
DialogContent,
@@ -79,6 +98,7 @@ const submittedByColumnWidth = 128;
const statusColumnWidth = 176;
const meetingTypeColumnWidth = 140;
const summaryColumnWidth = 280;
+const actionsColumnWidth = 80;
// ─── Helpers ─────────────────────────────────────────────────
@@ -135,6 +155,9 @@ export function CompanyFormPageClient({
const [isUploading, setIsUploading] = useState(false);
const [selectedFile, setSelectedFile] = useState
(null);
const fileInputRef = useRef(null);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [submissionToDelete, setSubmissionToDelete] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
const { data: activeMember } = useActiveMember();
const memberRoles = activeMember?.role?.split(',').map((role: string) => role.trim()) || [];
@@ -315,6 +338,44 @@ export function CompanyFormPageClient({
}
}, [selectedFile, isMeeting, formType, organizationId, query, globalMutate]);
+ const handleConfirmDelete = useCallback(async () => {
+ if (!submissionToDelete) return;
+
+ const submissionFormType = (submissionToDelete.formType ?? formType) as EvidenceFormType;
+ setIsDeleting(true);
+ try {
+ const response = await api.delete<{ success: boolean; id: string }>(
+ `/v1/evidence-forms/${submissionFormType}/submissions/${submissionToDelete.id}`,
+ organizationId,
+ );
+
+ if (response.error || !response.data?.success) {
+ throw new Error(response.error ?? 'Failed to delete submission');
+ }
+
+ toast.success('Submission deleted');
+ setDeleteDialogOpen(false);
+ setSubmissionToDelete(null);
+
+ if (isMeeting) {
+ for (const subType of MEETING_SUB_TYPES) {
+ globalMutate([`/v1/evidence-forms/${subType}${query}`, organizationId]);
+ }
+ for (const subType of MEETING_SUB_TYPES) {
+ globalMutate([`/v1/findings?evidenceFormType=${subType}`, organizationId]);
+ }
+ globalMutate([`/v1/findings?evidenceFormType=meeting`, organizationId]);
+ } else {
+ globalMutate([`/v1/evidence-forms/${formType}${query}`, organizationId]);
+ globalMutate([`/v1/findings?evidenceFormType=${formType}`, organizationId]);
+ }
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : 'Failed to delete submission');
+ } finally {
+ setIsDeleting(false);
+ }
+ }, [submissionToDelete, formType, isMeeting, organizationId, query, globalMutate]);
+
return (
{formType === 'access-request' &&
}
{showSummaryColumn &&
}
+ {isAdminOrOwner &&
}
@@ -417,6 +479,11 @@ export function CompanyFormPageClient({
)}
{showSummaryColumn && Summary}
+ {isAdminOrOwner && (
+
+ Actions
+
+ )}
@@ -475,6 +542,33 @@ export function CompanyFormPageClient({
)}
+ {isAdminOrOwner && (
+ e.stopPropagation()}>
+
+
+ e.stopPropagation()}
+ >
+
+
+
+ {
+ e.stopPropagation();
+ setSubmissionToDelete(submission);
+ setDeleteDialogOpen(true);
+ }}
+ >
+
+ Delete
+
+
+
+
+
+ )}
);
})}
@@ -541,6 +635,35 @@ export function CompanyFormPageClient({
+
+ {
+ setDeleteDialogOpen(open);
+ if (!open) setSubmissionToDelete(null);
+ }}
+ >
+
+
+ Delete submission
+
+ Are you sure you want to delete this submission? This action cannot be undone.
+
+
+
+ Cancel
+
+
+
+
);
}
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/DraggableCards.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/DraggableCards.tsx
deleted file mode 100644
index 60916819ec..0000000000
--- a/apps/app/src/app/(app)/[orgId]/frameworks/components/DraggableCards.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-'use client';
-
-import {
- DndContext,
- DragEndEvent,
- DragOverlay,
- DragStartEvent,
- PointerSensor,
- closestCenter,
- useSensor,
- useSensors,
-} from '@dnd-kit/core';
-import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable';
-import { useEffect, useState } from 'react';
-import { useCardOrder } from '../hooks/useCardOrder';
-import { SortableCard } from './SortableCard';
-
-interface DraggableCardsProps {
- children: React.ReactNode[];
- onReorder?: (newOrder: number[]) => void;
-}
-
-export function DraggableCards({ children, onReorder }: DraggableCardsProps) {
- const [items, setItems] = useState(children);
- const [activeId, setActiveId] = useState(null);
- const [mounted, setMounted] = useState(false);
- const { order, updateOrder } = useCardOrder(children.map((_, index) => index));
-
- // Ensure component is mounted before rendering drag-and-drop functionality
- useEffect(() => {
- setMounted(true);
- }, []);
-
- // Reorder items when order changes from localStorage
- useEffect(() => {
- if (order.length === children.length) {
- const reorderedItems = order.map((index) => children[index]);
- setItems(reorderedItems);
- }
- }, [order, children]);
-
- const sensors = useSensors(
- useSensor(PointerSensor, {
- activationConstraint: {
- distance: 8,
- },
- }),
- );
-
- const handleDragStart = (event: DragStartEvent) => {
- setActiveId(event.active.id as string);
- };
-
- const handleDragEnd = (event: DragEndEvent) => {
- const { active, over } = event;
-
- if (over && active.id !== over.id) {
- const oldIndex = items.findIndex((_, index) => `card-${index}` === active.id);
- const newIndex = items.findIndex((_, index) => `card-${index}` === over.id);
-
- const newItems = arrayMove(items, oldIndex, newIndex);
- setItems(newItems);
-
- // Update the stored order - map each position to the original card index
- const newOrder = newItems.map((item, newPosition) => {
- // Find which original card (from children) this item represents
- const originalIndex = children.findIndex((child) => child === item);
- return originalIndex;
- });
-
- console.log('Drag reorder:', {
- oldIndex,
- newIndex,
- newItems: newItems.map((_, i) => `card-${i}`),
- newOrder,
- childrenLength: children.length,
- });
-
- updateOrder(newOrder);
-
- // Call the onReorder callback with the new order
- if (onReorder) {
- onReorder(newOrder);
- }
- }
-
- setActiveId(null);
- };
-
- // Don't render drag-and-drop functionality until mounted to prevent hydration mismatch
- if (!mounted) {
- return (
-
- {children.map((child, index) => (
-
- {child}
-
- ))}
-
- );
- }
-
- return (
-
- `card-${index}`)}
- strategy={verticalListSortingStrategy}
- >
-
- {items.map((child, index) => (
-
- {child}
-
- ))}
-
-
-
-
- {activeId ? (
-
- {items.find((_, index) => `card-${index}` === activeId)}
-
- ) : null}
-
-
- );
-}
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx
index 9c69702e13..47bf84f5d5 100644
--- a/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx
+++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx
@@ -3,7 +3,6 @@
import { Finding, FrameworkEditorFramework, Policy, Task } from '@db';
import { FrameworkInstanceWithControls } from '../types';
import { ComplianceOverview } from './ComplianceOverview';
-import { DraggableCards } from './DraggableCards';
import { FindingsOverview } from './FindingsOverview';
import { FrameworksOverview } from './FrameworksOverview';
import { ToDoOverview } from './ToDoOverview';
@@ -84,7 +83,7 @@ export const Overview = ({
});
return (
-
+
-
+
);
};
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/SortableCard.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/SortableCard.tsx
deleted file mode 100644
index ad40b4a141..0000000000
--- a/apps/app/src/app/(app)/[orgId]/frameworks/components/SortableCard.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-
-import { useSortable } from '@dnd-kit/sortable';
-import { CSS } from '@dnd-kit/utilities';
-import { GripVertical } from 'lucide-react';
-
-interface SortableCardProps {
- id: string;
- children: React.ReactNode;
-}
-
-export function SortableCard({ id, children }: SortableCardProps) {
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
- id,
- });
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- opacity: isDragging ? 0.5 : 1,
- };
-
- return (
-
- {/* Drag Handle */}
-
-
-
-
- {/* Card Content */}
-
- {children}
-
-
- );
-}
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/hooks/useCardOrder.ts b/apps/app/src/app/(app)/[orgId]/frameworks/hooks/useCardOrder.ts
deleted file mode 100644
index b8283374e8..0000000000
--- a/apps/app/src/app/(app)/[orgId]/frameworks/hooks/useCardOrder.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-const STORAGE_KEY = 'frameworks-cards-order';
-
-export function useCardOrder(defaultOrder: number[]) {
- const [order, setOrder] = useState(() => {
- if (typeof window === 'undefined') return defaultOrder;
-
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- return stored ? JSON.parse(stored) : defaultOrder;
- } catch {
- return defaultOrder;
- }
- });
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(order));
- }
- }, [order]);
-
- const updateOrder = (newOrder: number[]) => {
- setOrder(newOrder);
- };
-
- return { order, updateOrder };
-}
diff --git a/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx b/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx
index 8be667d387..0f02fb7e69 100644
--- a/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx
+++ b/apps/app/src/app/(app)/[orgId]/integrations/components/PlatformIntegrations.tsx
@@ -486,6 +486,12 @@ export function PlatformIntegrations({ className, taskTemplates }: PlatformInteg
{provider.description}
+ {provider.category === 'Cloud' && (
+
+ This integration is used exclusively for Cloud Security Tests.
+
+ )}
+
{/* Mapped tasks */}
{provider.mappedTasks && provider.mappedTasks.length > 0 && (
diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx
index 2bfb92a3fe..9fda8e6d35 100644
--- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx
@@ -232,6 +232,8 @@ export const EmployeeDetails = ({
selected={joinDate}
onSelect={(date) => date && setJoinDate(date)}
captionLayout="dropdown"
+ fromYear={2000}
+ toYear={new Date().getFullYear()}
disabled={(date) => date > new Date()}
/>
diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx
index 5aa8c8a3a0..b9bcf6e3ca 100644
--- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx
@@ -47,6 +47,7 @@ export async function EmployeesOverview() {
where: {
organizationId: organizationId,
deactivated: false,
+ isActive: true,
},
include: {
user: true,
diff --git a/apps/app/src/app/(app)/[orgId]/people/page.tsx b/apps/app/src/app/(app)/[orgId]/people/page.tsx
index 80fc2a94f4..e1d3324d33 100644
--- a/apps/app/src/app/(app)/[orgId]/people/page.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/page.tsx
@@ -45,6 +45,7 @@ export default async function PeoplePage({ params }: { params: Promise<{ orgId:
where: {
organizationId: orgId,
deactivated: false,
+ isActive: true,
},
include: {
user: {
diff --git a/apps/app/src/app/(app)/[orgId]/security/components/SecuritySidebar.tsx b/apps/app/src/app/(app)/[orgId]/security/components/SecuritySidebar.tsx
index f62ac60b1c..b0c7fe8440 100644
--- a/apps/app/src/app/(app)/[orgId]/security/components/SecuritySidebar.tsx
+++ b/apps/app/src/app/(app)/[orgId]/security/components/SecuritySidebar.tsx
@@ -8,16 +8,10 @@ interface SecuritySidebarProps {
orgId: string;
}
-type SecurityNavItem = {
- id: string;
- label: string;
- path: string;
-};
-
export function SecuritySidebar({ orgId }: SecuritySidebarProps) {
const pathname = usePathname() ?? '';
- const items: SecurityNavItem[] = [
+ const items = [
{
id: 'penetration-tests',
label: 'Penetration Tests',
@@ -25,9 +19,7 @@ export function SecuritySidebar({ orgId }: SecuritySidebarProps) {
},
];
- const isPathActive = (path: string) => {
- return pathname.startsWith(path);
- };
+ const isPathActive = (path: string) => pathname.startsWith(path);
return (
diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.test.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.test.tsx
index 8d91573aca..7cb7d18537 100644
--- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.test.tsx
+++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.test.tsx
@@ -95,9 +95,6 @@ describe('PenetrationTestPageClient', () => {
it('renders completed report details and artifact links', () => {
const report: PentestRun = {
id: 'run_1',
- sandboxId: 'sandbox_1',
- workflowId: 'workflow_1',
- sessionId: 'session_1',
targetUrl: 'https://example.com',
repoUrl: 'https://github.com/org/repo',
status: 'completed',
@@ -106,8 +103,6 @@ describe('PenetrationTestPageClient', () => {
error: null,
temporalUiUrl: null,
webhookUrl: null,
- userId: 'user_1',
- organizationId: 'org_123',
};
reportMock.mockReturnValue({
@@ -131,46 +126,9 @@ describe('PenetrationTestPageClient', () => {
expect(screen.queryByText('Current progress')).toBeNull();
});
- it('shows sandbox placeholder when sandboxId is missing', () => {
- const report: PentestRun = {
- id: 'run_5',
- sandboxId: '',
- workflowId: 'workflow_5',
- sessionId: 'session_5',
- targetUrl: 'https://example.com',
- repoUrl: 'https://github.com/org/repo',
- status: 'completed',
- createdAt: '2026-02-26T18:00:00Z',
- updatedAt: '2026-02-25T18:30:00Z',
- error: null,
- temporalUiUrl: null,
- webhookUrl: null,
- userId: 'user_1',
- organizationId: 'org_123',
- };
-
- reportMock.mockReturnValue({
- report,
- isLoading: false,
- error: undefined,
- mutate: vi.fn(),
- });
- progressMock.mockReturnValue({
- progress: null,
- isLoading: false,
- });
-
- render();
-
- expect(screen.getByText('—')).toBeInTheDocument();
- });
-
it('shows repository placeholder when repoUrl is missing', () => {
const report: PentestRun = {
id: 'run_6',
- sandboxId: 'sandbox_6',
- workflowId: 'workflow_6',
- sessionId: 'session_6',
targetUrl: 'https://example.com',
repoUrl: null,
status: 'completed',
@@ -179,8 +137,6 @@ describe('PenetrationTestPageClient', () => {
error: null,
temporalUiUrl: null,
webhookUrl: null,
- userId: 'user_1',
- organizationId: 'org_123',
};
reportMock.mockReturnValue({
@@ -203,9 +159,6 @@ describe('PenetrationTestPageClient', () => {
it('renders running progress section when a live report is available', async () => {
const report: PentestRun = {
id: 'run_2',
- sandboxId: 'sandbox_2',
- workflowId: 'workflow_2',
- sessionId: 'session_2',
targetUrl: 'https://example.com',
repoUrl: 'https://github.com/org/repo',
status: 'running',
@@ -214,8 +167,6 @@ describe('PenetrationTestPageClient', () => {
error: null,
temporalUiUrl: null,
webhookUrl: null,
- userId: 'user_1',
- organizationId: 'org_123',
};
reportMock.mockReturnValue({
@@ -227,8 +178,6 @@ describe('PenetrationTestPageClient', () => {
progressMock.mockReturnValue({
progress: {
status: 'running',
- phase: 'scan',
- agent: null,
completedAgents: 1,
totalAgents: 2,
elapsedMs: 300,
@@ -240,16 +189,13 @@ describe('PenetrationTestPageClient', () => {
expect(screen.getByText('Running')).toBeInTheDocument();
expect(screen.getByText('Current progress')).toBeInTheDocument();
- expect(screen.getByText('scan (1/2)')).toBeInTheDocument();
+ expect(screen.getByText('In progress (1/2)')).toBeInTheDocument();
expect(screen.queryByText('Download PDF')).toBeNull();
});
- it('renders progress fallback text without phase and counts when data is incomplete', async () => {
+ it('renders progress fallback text when agent counts are unavailable', async () => {
const report: PentestRun = {
id: 'run_4',
- sandboxId: 'sandbox_4',
- workflowId: 'workflow_4',
- sessionId: 'session_4',
targetUrl: 'https://example.com',
repoUrl: 'https://github.com/org/repo',
status: 'running',
@@ -258,8 +204,6 @@ describe('PenetrationTestPageClient', () => {
error: null,
temporalUiUrl: null,
webhookUrl: null,
- userId: 'user_1',
- organizationId: 'org_123',
};
reportMock.mockReturnValue({
@@ -271,8 +215,6 @@ describe('PenetrationTestPageClient', () => {
progressMock.mockReturnValue({
progress: {
status: 'running',
- phase: null,
- agent: null,
completedAgents: '1' as unknown as number,
totalAgents: '2' as unknown as number,
elapsedMs: 400,
@@ -289,9 +231,6 @@ describe('PenetrationTestPageClient', () => {
it('allows progress updates to render from the progress hook contract', () => {
const report: PentestRun = {
id: 'run_3',
- sandboxId: 'sandbox_3',
- workflowId: 'workflow_3',
- sessionId: 'session_3',
targetUrl: 'https://example.com',
repoUrl: 'https://github.com/org/repo',
status: 'failed',
@@ -300,8 +239,6 @@ describe('PenetrationTestPageClient', () => {
error: 'Scan failed due to provider timeout',
temporalUiUrl: 'https://temporal.ui/session',
webhookUrl: null,
- userId: 'user_1',
- organizationId: 'org_123',
};
reportMock.mockReturnValue({
diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.tsx
index 3e82e327bd..50f2e0cd51 100644
--- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.tsx
+++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/penetration-test-page-client.tsx
@@ -77,6 +77,7 @@ export function PenetrationTestPageClient({ orgId, reportId }: PenetrationTestPa
const isInProgress = isReportInProgress(report.status);
const safeTemporalUiUrl =
report.temporalUiUrl ? toSafeExternalHttpUrl(report.temporalUiUrl) : null;
+ const runFailureReason = report.failedReason ?? report.error ?? null;
const openArtifact = async (path: string, filename?: string): Promise => {
try {
@@ -159,16 +160,12 @@ export function PenetrationTestPageClient({ orgId, reportId }: PenetrationTestPa
Last update
{formatReportDate(report.updatedAt)}
-
-
Sandbox
-
{report.sandboxId || '—'}
-
)}
@@ -176,11 +173,7 @@ export function PenetrationTestPageClient({ orgId, reportId }: PenetrationTestPa
) : null}
diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/actions/billing.ts b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/actions/billing.ts
new file mode 100644
index 0000000000..d415e677c0
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/actions/billing.ts
@@ -0,0 +1,304 @@
+'use server';
+
+import { auth } from '@/utils/auth';
+import { env } from '@/env.mjs';
+import { stripe } from '@/lib/stripe';
+import { db } from '@db';
+import { headers } from 'next/headers';
+
+async function requireOrgMember(orgId: string): Promise