From 90283b66be0539175773f46a5d2ac964d0edf408 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:11:48 -0400 Subject: [PATCH 01/31] chore: update error logs for GCP [dev] [Marfuen] mariano/eng-198-error-trying-to-run-cloud-tests --- .../providers/gcp-security.service.ts | 15 +++++++++++++++ packages/integrations/src/gcp/src/index.ts | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/apps/api/src/cloud-security/providers/gcp-security.service.ts b/apps/api/src/cloud-security/providers/gcp-security.service.ts index ecd830e42e..01f0663da9 100644 --- a/apps/api/src/cloud-security/providers/gcp-security.service.ts +++ b/apps/api/src/cloud-security/providers/gcp-security.service.ts @@ -125,6 +125,21 @@ export class GCPSecurityService { ); } + if (errorText.includes('Security Command Center Legacy')) { + throw new Error( + 'Security Command Center Legacy has been disabled by Google. ' + + 'Please activate the Standard or Premium tier in your GCP console: ' + + 'Security > Security Command Center > Settings.', + ); + } + + if (errorText.includes('Security Command Center API has not been used')) { + throw new Error( + 'The Security Command Center API is not enabled for this project. ' + + 'Enable it in the GCP console: APIs & Services > Enable APIs > Security Command Center API.', + ); + } + throw new Error(`GCP API error (${response.status}): ${errorText}`); } diff --git a/packages/integrations/src/gcp/src/index.ts b/packages/integrations/src/gcp/src/index.ts index aa957a38b9..cea6dfd391 100644 --- a/packages/integrations/src/gcp/src/index.ts +++ b/packages/integrations/src/gcp/src/index.ts @@ -99,6 +99,15 @@ async function fetch(credentials: GCPCredentials): Promise { if (!res.ok) { const errorText = await res.text(); + + if (errorText.includes('Security Command Center Legacy')) { + throw new Error( + 'Security Command Center Legacy has been disabled by Google. ' + + 'Please activate the Standard or Premium tier in your GCP console: ' + + 'Security > Security Command Center > Settings.', + ); + } + throw new Error(`Failed to fetch findings: ${errorText}`); } From f461c4ddf6672e3be17cbaef09a1a4077c1f99eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:12:51 -0400 Subject: [PATCH 02/31] fix(company): make Access Request form options in Documents (#2369) Co-authored-by: chasprowebdev Co-authored-by: Mariano Fuentes --- packages/company/src/evidence-forms/definitions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/company/src/evidence-forms/definitions.ts b/packages/company/src/evidence-forms/definitions.ts index b262789fee..409a0ad06c 100644 --- a/packages/company/src/evidence-forms/definitions.ts +++ b/packages/company/src/evidence-forms/definitions.ts @@ -63,6 +63,7 @@ export const evidenceFormDefinitions: Record Date: Tue, 31 Mar 2026 10:13:37 -0400 Subject: [PATCH 03/31] fix(app): comment button gets disabled with numbered formatting (#2368) Co-authored-by: chasprowebdev Co-authored-by: Mariano Fuentes --- .../src/components/comments/CommentForm.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/apps/app/src/components/comments/CommentForm.tsx b/apps/app/src/components/comments/CommentForm.tsx index 54efed324f..eecb7e03f7 100644 --- a/apps/app/src/components/comments/CommentForm.tsx +++ b/apps/app/src/components/comments/CommentForm.tsx @@ -136,22 +136,17 @@ export function CommentForm({ entityId, entityType, mentionResource, organizatio // Check if comment has content const hasContent = useCallback((content: JSONContent | null): boolean => { - if (!content || !content.content) return false; - - // Check if there's any text content - const hasText = content.content.some((node) => { - if (node.type === 'paragraph' && node.content) { - return node.content.some((child) => { - if (child.type === 'text' && child.text?.trim()) return true; - if (child.type === 'mention') return true; // Mentions count as content - return false; - }); - } + if (!content) return false; + + const hasContentInNode = (node: JSONContent): boolean => { if (node.type === 'mention') return true; - return false; - }); + if (node.type === 'text' && node.text?.trim()) return true; + + if (!node.content || node.content.length === 0) return false; + return node.content.some(hasContentInNode); + }; - return hasText; + return hasContentInNode(content); }, []); const handleCommentSubmit = async () => { From b82438452c3d5ca4a91d46318b694f5b601c6f83 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 31 Mar 2026 10:56:40 -0400 Subject: [PATCH 04/31] chore(email): remove unused next dependency (#2407) The `next` package was listed as a dependency in `packages/email` but was never imported or used anywhere in the package. It is not a peer dependency of any of the package's actual dependencies (react-email, resend, etc.). Removing it to avoid unnecessary dependency maintenance. Co-authored-by: Claude Opus 4.6 (1M context) --- bun.lock | 25 ------------------------- packages/email/package.json | 1 - 2 files changed, 26 deletions(-) diff --git a/bun.lock b/bun.lock index e108a5bb98..f3c491945a 100644 --- a/bun.lock +++ b/bun.lock @@ -525,7 +525,6 @@ "@react-email/tailwind": "1.0.5", "@trycompai/utils": "workspace:*", "date-fns": "^4.1.0", - "next": "^15.4.6", "react-email": "^4.0.15", "resend": "^4.4.1", "responsive-react-email": "^0.0.5", @@ -6871,8 +6870,6 @@ "@trycompai/device-agent/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], - "@trycompai/email/next": ["next@15.5.9", "", { "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.7", "@next/swc-darwin-x64": "15.5.7", "@next/swc-linux-arm64-gnu": "15.5.7", "@next/swc-linux-arm64-musl": "15.5.7", "@next/swc-linux-x64-gnu": "15.5.7", "@next/swc-linux-x64-musl": "15.5.7", "@next/swc-win32-arm64-msvc": "15.5.7", "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg=="], - "@trycompai/email/resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="], "@trycompai/framework-editor/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], @@ -8479,26 +8476,6 @@ "@trycompai/device-agent/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@trycompai/email/next/@next/env": ["@next/env@15.5.9", "", {}, "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg=="], - - "@trycompai/email/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw=="], - - "@trycompai/email/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg=="], - - "@trycompai/email/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA=="], - - "@trycompai/email/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw=="], - - "@trycompai/email/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw=="], - - "@trycompai/email/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA=="], - - "@trycompai/email/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ=="], - - "@trycompai/email/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw=="], - - "@trycompai/email/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "@trycompai/email/resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], "@trycompai/framework-editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -9433,8 +9410,6 @@ "@trycompai/auth/better-auth/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - "@trycompai/email/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "@trycompai/email/resend/@react-email/render/prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], "@trycompai/framework-editor/better-auth/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], diff --git a/packages/email/package.json b/packages/email/package.json index 8667c88a14..bd972ed61c 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -8,7 +8,6 @@ "@react-email/tailwind": "1.0.5", "@trycompai/utils": "workspace:*", "date-fns": "^4.1.0", - "next": "^15.4.6", "react-email": "^4.0.15", "resend": "^4.4.1", "responsive-react-email": "^0.0.5" From 645dd489eb5dded7358eb9f4551484ca41dca569 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:59:04 -0400 Subject: [PATCH 05/31] chore(deps-dev): bump electron (#2345) Bumps the npm_and_yarn group with 1 update in the /packages/device-agent directory: [electron](https://github.com/electron/electron). Updates `electron` from 33.4.0 to 35.7.5 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v33.4.0...v35.7.5) --- updated-dependencies: - dependency-name: electron dependency-version: 35.7.5 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mariano Fuentes --- packages/device-agent/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/device-agent/package.json b/packages/device-agent/package.json index 2cebc42913..dbe0e63828 100644 --- a/packages/device-agent/package.json +++ b/packages/device-agent/package.json @@ -29,7 +29,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.5.2", - "electron": "33.4.0", + "electron": "35.7.5", "electron-builder": "^25.1.8", "electron-vite": "^3.1.0", "react": "^19.1.0", From 12e5e3a9c4d1f9ec81659c49bf9714b7d63af8d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:03:57 -0400 Subject: [PATCH 06/31] fix: Enable 'Ready for Review' menu for client on Document Finding (#2404) * fix(app): pass isPlatformAdmin value to FindingItem to make 'Ready for Review' enabled by client * fix(app): remove isAuditor and add canSetReadyForReview param to FindingItem * fix(app): remove isPlatformAdmin prop from FindingItem --------- Co-authored-by: chasprowebdev Co-authored-by: Mariano Fuentes --- .../app/(app)/[orgId]/documents/[formType]/page.tsx | 13 +++++++++++++ .../documents/components/CompanyFormPageClient.tsx | 4 ++-- .../components/DocumentFindingsSection.tsx | 5 +++-- .../[taskId]/components/findings/FindingItem.tsx | 10 +++------- .../[taskId]/components/findings/FindingsList.tsx | 3 +-- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/documents/[formType]/page.tsx b/apps/app/src/app/(app)/[orgId]/documents/[formType]/page.tsx index 45fa4f4090..4fd1326fb9 100644 --- a/apps/app/src/app/(app)/[orgId]/documents/[formType]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/documents/[formType]/page.tsx @@ -4,6 +4,8 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; import { Suspense } from 'react'; import { evidenceFormDefinitions, evidenceFormTypeSchema } from '../forms'; +import { auth } from '@/utils/auth'; +import { headers } from 'next/headers'; export default async function CompanyFormDetailPage({ params, @@ -19,6 +21,16 @@ export default async function CompanyFormDetailPage({ const formDefinition = evidenceFormDefinitions[parsedType.data]; + let isPlatformAdmin = false; + + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (session?.user?.id) { + isPlatformAdmin = session.user.role === 'admin'; + } + return ( 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 73927bba3a..4b577b9246 100644 --- a/apps/app/src/app/(app)/[orgId]/documents/components/CompanyFormPageClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/documents/components/CompanyFormPageClient.tsx @@ -145,7 +145,7 @@ async function evidenceFormFetcher([endpoint, orgId]: readonly [ export function CompanyFormPageClient({ organizationId, formType, - isPlatformAdmin, + isPlatformAdmin = false, }: { organizationId: string; formType: EvidenceFormType; @@ -579,7 +579,7 @@ export function CompanyFormPageClient({ - + diff --git a/apps/app/src/app/(app)/[orgId]/documents/components/DocumentFindingsSection.tsx b/apps/app/src/app/(app)/[orgId]/documents/components/DocumentFindingsSection.tsx index 590baf1acc..b98a61001d 100644 --- a/apps/app/src/app/(app)/[orgId]/documents/components/DocumentFindingsSection.tsx +++ b/apps/app/src/app/(app)/[orgId]/documents/components/DocumentFindingsSection.tsx @@ -25,10 +25,12 @@ const STATUS_ORDER: Record = { interface DocumentFindingsSectionProps { formType: EvidenceFormType; + isPlatformAdmin?: boolean; } export function DocumentFindingsSection({ formType, + isPlatformAdmin = false, }: DocumentFindingsSectionProps) { const { hasPermission } = usePermissions(); const { data: activeMember } = useActiveMember(); @@ -226,11 +228,10 @@ export function DocumentFindingsSection({ setExpandedId(expandedId === finding.id ? null : finding.id)} onStatusChange={(status, revisionNote) => handleStatusChange(finding.id, status, revisionNote) diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/findings/FindingItem.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/findings/FindingItem.tsx index 8ce72d3a74..2f54d60479 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/findings/FindingItem.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/findings/FindingItem.tsx @@ -37,9 +37,8 @@ interface FindingItemProps { isExpanded: boolean; canChangeStatus: boolean; canSetRestrictedStatus: boolean; + canSetReadyForReview: boolean; canDelete?: boolean; - isAuditor: boolean; - isPlatformAdmin: boolean; isTarget?: boolean; // Whether this finding is the navigation target onToggleExpand: () => void; onStatusChange: (status: FindingStatus, revisionNote?: string) => Promise | void; @@ -55,9 +54,8 @@ export function FindingItem({ isExpanded, canChangeStatus, canSetRestrictedStatus, + canSetReadyForReview, canDelete = canSetRestrictedStatus, - isAuditor, - isPlatformAdmin, isTarget = false, onToggleExpand, onStatusChange, @@ -152,8 +150,6 @@ export function FindingItem({ // - Auditors can set: open, needs_revision, closed // - Non-auditor admins/owners can set: open, ready_for_review const statusOptions = useMemo(() => { - const canSetReadyForReview = isPlatformAdmin || !isAuditor; - return [ { value: FindingStatus.open, label: 'Open', disabled: false }, { @@ -175,7 +171,7 @@ export function FindingItem({ hint: !canSetRestrictedStatus ? 'Auditor only' : undefined, }, ]; - }, [isPlatformAdmin, isAuditor, canSetRestrictedStatus]); + }, [canSetRestrictedStatus, canSetReadyForReview]); return (
setExpandedId(expandedId === finding.id ? null : finding.id)} onStatusChange={(status, revisionNote) => From a04c48627d79631aa718947bfacb9fb724ebb502 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 31 Mar 2026 16:53:58 -0400 Subject: [PATCH 07/31] feat: remove Ramp integration entirely Ramp integration is no longer needed. This removes all Ramp-related code across the stack: manifest, API services/controllers, sync endpoints, frontend components (role mapping UI, sync provider), and scheduled sync. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ramp-role-mapping.controller.ts | 196 ------ .../controllers/sync-gws.controller.spec.ts | 4 - .../controllers/sync.controller.ts | 559 +----------------- .../integration-platform.module.ts | 6 - .../services/generic-employee-sync.service.ts | 2 +- .../services/ramp-api.service.ts | 188 ------ .../services/ramp-role-mapping.service.ts | 235 -------- .../sync-employees-schedule.ts | 38 -- .../integrations/data/categories/hr.ts | 13 - .../all/components/RampRoleMappingSheet.tsx | 147 ----- .../all/components/TeamMembersClient.tsx | 40 +- .../(app)/[orgId]/people/all/data/queries.ts | 12 +- .../people/all/hooks/useEmployeeSync.ts | 135 +---- .../app/src/app/(app)/[orgId]/people/page.tsx | 12 +- .../components/RampRoleMappingContent.tsx | 195 ------ .../components/RampRoleMappingRow.tsx | 233 -------- .../components/RoleMappingTab.tsx | 69 --- .../prisma/schema/integration-sync-log.prisma | 2 +- packages/integration-platform/src/index.ts | 11 - .../manifests/ramp/checks/employee-sync.ts | 163 ----- .../src/manifests/ramp/checks/index.ts | 1 - .../src/manifests/ramp/index.ts | 55 -- .../src/manifests/ramp/types.ts | 70 --- .../src/registry/index.ts | 2 - 24 files changed, 11 insertions(+), 2377 deletions(-) delete mode 100644 apps/api/src/integration-platform/controllers/ramp-role-mapping.controller.ts delete mode 100644 apps/api/src/integration-platform/services/ramp-api.service.ts delete mode 100644 apps/api/src/integration-platform/services/ramp-role-mapping.service.ts delete mode 100644 apps/app/src/app/(app)/[orgId]/people/all/components/RampRoleMappingSheet.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingContent.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingRow.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RoleMappingTab.tsx delete mode 100644 packages/integration-platform/src/manifests/ramp/checks/employee-sync.ts delete mode 100644 packages/integration-platform/src/manifests/ramp/checks/index.ts delete mode 100644 packages/integration-platform/src/manifests/ramp/index.ts delete mode 100644 packages/integration-platform/src/manifests/ramp/types.ts diff --git a/apps/api/src/integration-platform/controllers/ramp-role-mapping.controller.ts b/apps/api/src/integration-platform/controllers/ramp-role-mapping.controller.ts deleted file mode 100644 index 60977367eb..0000000000 --- a/apps/api/src/integration-platform/controllers/ramp-role-mapping.controller.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - Controller, - Post, - Get, - Query, - Body, - HttpException, - HttpStatus, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiSecurity } from '@nestjs/swagger'; -import { HybridAuthGuard } from '../../auth/hybrid-auth.guard'; -import { PermissionGuard } from '../../auth/permission.guard'; -import { RequirePermission } from '../../auth/require-permission.decorator'; -import { OrganizationId } from '../../auth/auth-context.decorator'; -import { db } from '@db'; -import { ConnectionRepository } from '../repositories/connection.repository'; -import { RampRoleMappingService } from '../services/ramp-role-mapping.service'; -import { IntegrationSyncLoggerService } from '../services/integration-sync-logger.service'; -import { RampApiService } from '../services/ramp-api.service'; -import { type RoleMappingEntry } from '@trycompai/integration-platform'; - -@Controller({ path: 'integrations/sync/ramp', version: '1' }) -@ApiTags('Integrations') -@UseGuards(HybridAuthGuard, PermissionGuard) -@ApiSecurity('apikey') -export class RampRoleMappingController { - constructor( - private readonly connectionRepository: ConnectionRepository, - private readonly roleMappingService: RampRoleMappingService, - private readonly syncLoggerService: IntegrationSyncLoggerService, - private readonly rampApiService: RampApiService, - ) {} - - @Post('discover-roles') - @RequirePermission('integration', 'update') - async discoverRoles( - @OrganizationId() organizationId: string, - @Query('connectionId') connectionId: string, - @Query('refresh') refresh?: string, - ) { - if (!connectionId) { - throw new HttpException('connectionId is required', HttpStatus.BAD_REQUEST); - } - - const connection = await this.connectionRepository.findById(connectionId); - if (!connection || connection.organizationId !== organizationId) { - throw new HttpException('Connection not found', HttpStatus.NOT_FOUND); - } - - const shouldRefresh = refresh === 'true'; - let discoveredRoles: Array<{ role: string; userCount: number }>; - - // Use cached roles unless refresh is requested - const cachedRoles = shouldRefresh - ? null - : await this.roleMappingService.getCachedDiscoveredRoles(connectionId); - - if (cachedRoles) { - discoveredRoles = cachedRoles; - } else { - const logId = await this.syncLoggerService.startLog({ - connectionId, - organizationId, - provider: 'ramp', - eventType: 'role_discovery', - triggeredBy: 'manual', - }); - - try { - const accessToken = await this.rampApiService.getAccessToken(connectionId, organizationId); - const users = await this.rampApiService.fetchUsers(accessToken); - - const roleCounts = new Map(); - for (const user of users) { - const role = user.role ?? 'UNKNOWN'; - roleCounts.set(role, (roleCounts.get(role) ?? 0) + 1); - } - - discoveredRoles = Array.from(roleCounts.entries()) - .map(([role, userCount]) => ({ role, userCount })) - .sort((a, b) => b.userCount - a.userCount); - - // Cache the discovered roles (preserve existing mapping if any) - const existingMapping = await this.roleMappingService.getSavedMapping(connectionId); - if (existingMapping) { - await this.roleMappingService.saveMapping( - connectionId, - existingMapping, - discoveredRoles, - ); - } else { - await this.roleMappingService.saveDiscoveredRoles(connectionId, discoveredRoles); - } - - await this.syncLoggerService.completeLog(logId, { - rolesDiscovered: discoveredRoles.length, - totalUsers: users.length, - }); - } catch (error) { - await this.syncLoggerService.failLog( - logId, - error instanceof Error ? error.message : String(error), - ); - throw error; - } - } - - const rampRoleNames = discoveredRoles.map((r) => r.role); - const defaultMapping = this.roleMappingService.getDefaultMapping(rampRoleNames); - const existingMapping = await this.roleMappingService.getSavedMapping(connectionId); - - // Fetch existing custom roles for this org with their permissions - const customRoles = await db.organizationRole.findMany({ - where: { organizationId }, - select: { name: true, permissions: true, obligations: true }, - orderBy: { name: 'asc' }, - }); - - const existingCustomRoles = customRoles.map((r) => ({ - name: r.name, - permissions: JSON.parse(r.permissions) as Record, - obligations: JSON.parse(r.obligations) as Record, - })); - - return { discoveredRoles, defaultMapping, existingMapping, existingCustomRoles }; - } - - @Post('role-mapping') - @RequirePermission('integration', 'update') - async saveRoleMapping( - @OrganizationId() organizationId: string, - @Body() body: { connectionId: string; mapping: RoleMappingEntry[] }, - ) { - const { connectionId, mapping } = body; - - if (!connectionId || !Array.isArray(mapping)) { - throw new HttpException( - 'connectionId and mapping are required', - HttpStatus.BAD_REQUEST, - ); - } - - const connection = await this.connectionRepository.findById(connectionId); - if (!connection || connection.organizationId !== organizationId) { - throw new HttpException('Connection not found', HttpStatus.NOT_FOUND); - } - - const logId = await this.syncLoggerService.startLog({ - connectionId, - organizationId, - provider: 'ramp', - eventType: 'role_mapping_save', - triggeredBy: 'manual', - }); - - try { - // Create custom roles in the database - await this.roleMappingService.ensureCustomRolesExist(organizationId, mapping); - - // Save mapping to connection variables (preserve existing discovered roles) - await this.roleMappingService.saveMapping(connectionId, mapping); - - await this.syncLoggerService.completeLog(logId, { - mappingCount: mapping.length, - }); - - return { success: true, mapping }; - } catch (error) { - await this.syncLoggerService.failLog( - logId, - error instanceof Error ? error.message : String(error), - ); - throw error; - } - } - - @Get('role-mapping') - @RequirePermission('integration', 'read') - async getRoleMapping( - @OrganizationId() organizationId: string, - @Query('connectionId') connectionId: string, - ) { - if (!connectionId) { - throw new HttpException('connectionId is required', HttpStatus.BAD_REQUEST); - } - - const connection = await this.connectionRepository.findById(connectionId); - if (!connection || connection.organizationId !== organizationId) { - throw new HttpException('Connection not found', HttpStatus.NOT_FOUND); - } - - const mapping = await this.roleMappingService.getSavedMapping(connectionId); - return { mapping }; - } -} diff --git a/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts b/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts index aaba4ac2ba..4320387f89 100644 --- a/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts +++ b/apps/api/src/integration-platform/controllers/sync-gws.controller.spec.ts @@ -5,9 +5,7 @@ import { PermissionGuard } from '../../auth/permission.guard'; import { ConnectionRepository } from '../repositories/connection.repository'; import { CredentialVaultService } from '../services/credential-vault.service'; import { OAuthCredentialsService } from '../services/oauth-credentials.service'; -import { RampRoleMappingService } from '../services/ramp-role-mapping.service'; import { IntegrationSyncLoggerService } from '../services/integration-sync-logger.service'; -import { RampApiService } from '../services/ramp-api.service'; import { db } from '@db'; jest.mock('@db', () => ({ @@ -97,12 +95,10 @@ describe('SyncController - Google Workspace employees', () => { { provide: ConnectionRepository, useValue: mockConnectionRepo }, { provide: CredentialVaultService, useValue: mockCredentialVault }, { provide: OAuthCredentialsService, useValue: mockOAuthCredentials }, - { provide: RampRoleMappingService, useValue: {} }, { provide: IntegrationSyncLoggerService, useValue: { logSync: jest.fn() }, }, - { provide: RampApiService, useValue: {} }, ], }) .overrideGuard(HybridAuthGuard) diff --git a/apps/api/src/integration-platform/controllers/sync.controller.ts b/apps/api/src/integration-platform/controllers/sync.controller.ts index 6907fdb440..3c16559545 100644 --- a/apps/api/src/integration-platform/controllers/sync.controller.ts +++ b/apps/api/src/integration-platform/controllers/sync.controller.ts @@ -31,15 +31,9 @@ import { parseSyncFilterTerms, interpretDeclarativeSync, type OAuthConfig, - type RampUser, - type RampUserStatus, - type RampUsersResponse, - type RoleMappingEntry, type SyncDefinition, } from '@trycompai/integration-platform'; -import { RampRoleMappingService } from '../services/ramp-role-mapping.service'; import { IntegrationSyncLoggerService } from '../services/integration-sync-logger.service'; -import { RampApiService } from '../services/ramp-api.service'; import { GenericEmployeeSyncService } from '../services/generic-employee-sync.service'; import { DynamicIntegrationRepository } from '../repositories/dynamic-integration.repository'; import { CheckRunRepository } from '../repositories/check-run.repository'; @@ -80,9 +74,7 @@ export class SyncController { private readonly connectionRepository: ConnectionRepository, private readonly credentialVaultService: CredentialVaultService, private readonly oauthCredentialsService: OAuthCredentialsService, - private readonly rampRoleMappingService: RampRoleMappingService, private readonly syncLoggerService: IntegrationSyncLoggerService, - private readonly rampApiService: RampApiService, private readonly genericSyncService: GenericEmployeeSyncService, private readonly dynamicIntegrationRepo: DynamicIntegrationRepository, private readonly checkRunRepo: CheckRunRepository, @@ -349,7 +341,7 @@ export class SyncController { | 'reactivated' | 'error'; reason?: string; - rampStatus?: RampUserStatus | 'USER_MISSING'; + providerStatus?: string; }>, }; @@ -802,7 +794,7 @@ export class SyncController { | 'reactivated' | 'error'; reason?: string; - rampStatus?: RampUserStatus | 'USER_MISSING'; + providerStatus?: string; }>, }; @@ -956,524 +948,6 @@ export class SyncController { }; } - /** - * Sync employees from Ramp - */ - @Post('ramp/employees') - @RequirePermission('integration', 'update') - async syncRampEmployees( - @OrganizationId() organizationId: string, - @Query('connectionId') connectionId: string, - @AuthContext() authContext: AuthContextType, - ) { - if (!connectionId) { - throw new HttpException( - 'connectionId is required', - HttpStatus.BAD_REQUEST, - ); - } - - const connection = await this.connectionRepository.findById(connectionId); - if (!connection) { - throw new HttpException('Connection not found', HttpStatus.NOT_FOUND); - } - - if (connection.organizationId !== organizationId) { - throw new HttpException('Connection not found', HttpStatus.NOT_FOUND); - } - - const provider = await db.integrationProvider.findUnique({ - where: { id: connection.providerId }, - }); - - if (!provider || provider.slug !== 'ramp') { - throw new HttpException( - 'This endpoint only supports Ramp connections', - HttpStatus.BAD_REQUEST, - ); - } - - const triggeredBy = - authContext.authType === 'service' - ? 'scheduled' - : authContext.authType === 'api-key' - ? 'api' - : 'manual'; - - const logId = await this.syncLoggerService.startLog({ - connectionId, - organizationId, - provider: 'ramp', - eventType: 'employee_sync', - triggeredBy, - userId: authContext.userId ?? undefined, - }); - - try { - return await this.syncRampEmployeesInner( - organizationId, - connectionId, - authContext, - connection, - logId, - ); - } catch (error) { - await this.syncLoggerService.failLog( - logId, - error instanceof Error ? error.message : String(error), - ); - throw error; - } - } - - private async syncRampEmployeesInner( - organizationId: string, - connectionId: string, - authContext: AuthContextType, - connection: { variables: unknown }, - logId: string, - ) { - const accessToken = await this.rampApiService.getAccessToken(connectionId, organizationId); - - const baseUsers = await this.rampApiService.fetchUsers(accessToken); - const suspendedUsers = await this.rampApiService.fetchUsers(accessToken, 'USER_SUSPENDED'); - const users = [...baseUsers, ...suspendedUsers]; - - // Filter out non-syncable statuses (pending invites, onboarding, expired) - const syncableStatuses = new Set([ - 'USER_ACTIVE', - 'USER_INACTIVE', - 'USER_SUSPENDED', - ]); - const skippedStatuses = users.filter( - (u) => u.status && !syncableStatuses.has(u.status), - ); - if (skippedStatuses.length > 0) { - this.logger.log( - `Skipping ${skippedStatuses.length} Ramp users with non-syncable statuses (INVITE_PENDING, INVITE_EXPIRED, USER_ONBOARDING)`, - ); - } - - const activeUsers = users.filter((u) => u.status === 'USER_ACTIVE'); - const inactiveUsers = users.filter((u) => u.status === 'USER_INACTIVE'); - - const activeEmails = new Set( - activeUsers - .map((u) => u.email?.toLowerCase()) - .filter((email): email is string => Boolean(email)), - ); - const inactiveEmails = new Set( - inactiveUsers - .map((u) => u.email?.toLowerCase()) - .filter((email): email is string => Boolean(email)), - ); - const suspendedEmails = new Set( - suspendedUsers - .map((u) => u.email?.toLowerCase()) - .filter((email): email is string => Boolean(email)), - ); - - this.logger.log( - `Found ${activeUsers.length} active, ${inactiveUsers.length} inactive, and ${suspendedUsers.length} suspended Ramp users`, - ); - - // Load role mapping from connection variables - const connectionVars = (connection.variables ?? {}) as Record< - string, - unknown - >; - let roleMapping = Array.isArray(connectionVars.role_mapping) - ? (connectionVars.role_mapping as RoleMappingEntry[]) - : null; - - if (!roleMapping) { - const isAutomatedSync = authContext.authType === 'service'; - - if (!isAutomatedSync) { - // Manual sync — prompt user to configure mapping via UI - await this.syncLoggerService.completeLog(logId, { - requiresRoleMapping: true, - message: 'Role mapping is not configured', - }); - return { - success: false, - requiresRoleMapping: true, - message: - 'Role mapping is not configured. Please configure role mapping before syncing.', - }; - } - - // Automated sync (cron) — auto-generate default mapping - const allRampRolesForDefault = [ - ...new Set( - activeUsers - .map((u) => u.role) - .filter((r): r is string => Boolean(r)), - ), - ]; - - if (allRampRolesForDefault.length === 0) { - this.logger.warn( - 'No Ramp roles found to auto-generate mapping', - ); - const emptyResult = { - totalFound: 0, - imported: 0, - skipped: 0, - deactivated: 0, - reactivated: 0, - errors: 0, - details: [], - }; - await this.syncLoggerService.completeLog(logId, emptyResult); - return { success: true, ...emptyResult }; - } - - const defaultEntries = - this.rampRoleMappingService.getDefaultMapping( - allRampRolesForDefault, - ); - - await this.rampRoleMappingService.ensureCustomRolesExist( - organizationId, - defaultEntries, - ); - await this.rampRoleMappingService.saveMapping( - connectionId, - defaultEntries, - ); - - roleMapping = defaultEntries; - - this.logger.log( - `Auto-generated default role mapping for Ramp sync (${defaultEntries.length} roles)`, - ); - } - - // Discover all Ramp roles in this batch and auto-create mappings for unknown ones - const allRampRoles = new Set( - activeUsers - .map((u) => u.role) - .filter((r): r is string => Boolean(r)), - ); - const mappedRoles = new Set(roleMapping.map((m) => m.rampRole)); - const newRoles = [...allRampRoles].filter((r) => !mappedRoles.has(r)); - - if (newRoles.length > 0) { - this.logger.log( - `Found ${newRoles.length} new Ramp roles not in mapping: ${newRoles.join(', ')}`, - ); - - const newEntries = - this.rampRoleMappingService.getDefaultMapping(newRoles); - - // Create custom roles in DB for new entries - await this.rampRoleMappingService.ensureCustomRolesExist( - organizationId, - newEntries, - ); - - // Add to mapping and save - const updatedMapping = [...roleMapping, ...newEntries]; - await this.rampRoleMappingService.saveMapping( - connectionId, - updatedMapping, - ); - - // Use the updated mapping - roleMapping.push(...newEntries); - } - - const roleMappingLookup = new Map( - roleMapping.map((m) => [m.rampRole, m.compRole]), - ); - - const results = { - imported: 0, - updated: 0, - skipped: 0, - deactivated: 0, - reactivated: 0, - errors: 0, - details: [] as Array<{ - email: string; - status: - | 'imported' - | 'updated' - | 'skipped' - | 'deactivated' - | 'reactivated' - | 'error'; - reason?: string; - rampStatus?: RampUserStatus | 'USER_MISSING'; - }>, - }; - - for (const rampUser of activeUsers) { - const normalizedEmail = rampUser.email?.toLowerCase(); - if (!normalizedEmail) { - continue; - } - - try { - // Try external ID match first (handles email changes) - let existingMember = rampUser.id - ? await db.member.findFirst({ - where: { - organizationId, - externalUserId: rampUser.id, - externalUserSource: 'ramp', - }, - }) - : null; - - // Fall back to email match - if (!existingMember) { - const existingUser = await db.user.findUnique({ - where: { email: normalizedEmail }, - }); - if (existingUser) { - existingMember = await db.member.findFirst({ - where: { organizationId, userId: existingUser.id }, - }); - } - } - - if (existingMember) { - const mappedRole = - roleMappingLookup.get(rampUser.role ?? '') ?? 'employee'; - - // Build update data: backfill external ID + update role if changed - const updateData: Record = {}; - - if ( - rampUser.id && - (!existingMember.externalUserId || - existingMember.externalUserSource !== 'ramp') - ) { - updateData.externalUserId = rampUser.id; - updateData.externalUserSource = 'ramp'; - } - - // Update role if it changed (but don't downgrade privileged roles) - const currentRoles = existingMember.role - .split(',') - .map((r) => r.trim().toLowerCase()); - const isPrivileged = - currentRoles.includes('owner') || - currentRoles.includes('admin') || - currentRoles.includes('auditor'); - - if (!isPrivileged && existingMember.role !== mappedRole) { - updateData.role = mappedRole; - } - - if (existingMember.deactivated) { - updateData.deactivated = false; - updateData.isActive = true; - - await db.member.update({ - where: { id: existingMember.id }, - data: updateData, - }); - results.reactivated++; - results.details.push({ - email: normalizedEmail, - status: 'reactivated', - reason: 'User is active again in Ramp', - }); - } else if (Object.keys(updateData).length > 0) { - await db.member.update({ - where: { id: existingMember.id }, - data: updateData, - }); - if (updateData.role) { - results.updated++; - results.details.push({ - email: normalizedEmail, - status: 'updated', - reason: `Role updated to ${mappedRole}`, - }); - } else { - results.skipped++; - results.details.push({ - email: normalizedEmail, - status: 'skipped', - reason: 'Already a member (external ID backfilled)', - }); - } - } else { - results.skipped++; - results.details.push({ - email: normalizedEmail, - status: 'skipped', - reason: 'Already a member', - }); - } - continue; - } - - // Create new user if needed - let existingUser = await db.user.findUnique({ - where: { email: normalizedEmail }, - }); - - if (!existingUser) { - const displayName = - `${rampUser.first_name ?? ''} ${rampUser.last_name ?? ''}`.trim() || - normalizedEmail.split('@')[0]; - - existingUser = await db.user.create({ - data: { - email: normalizedEmail, - name: displayName, - emailVerified: true, - }, - }); - } - - const mappedRole = - roleMappingLookup.get(rampUser.role ?? '') ?? 'employee'; - - await db.member.create({ - data: { - organizationId, - userId: existingUser.id, - role: mappedRole, - isActive: true, - externalUserId: rampUser.id || null, - externalUserSource: rampUser.id ? 'ramp' : null, - }, - }); - - results.imported++; - results.details.push({ - email: normalizedEmail, - status: 'imported', - }); - } catch (error) { - this.logger.error(`Error importing Ramp user: ${error}`); - results.errors++; - results.details.push({ - email: normalizedEmail, - status: 'error', - reason: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - - const allOrgMembers = await db.member.findMany({ - where: { - organizationId, - deactivated: false, - }, - include: { - user: true, - }, - }); - - const rampDomains = new Set( - users - .map((u) => u.email?.split('@')[1]?.toLowerCase()) - .filter((domain): domain is string => Boolean(domain)), - ); - - for (const member of allOrgMembers) { - const memberEmail = member.user.email.toLowerCase(); - const memberDomain = memberEmail.split('@')[1]; - - if (!memberDomain || !rampDomains.has(memberDomain)) { - continue; - } - - // Safety guard: never auto-deactivate privileged members via sync - const memberRoles = member.role - .split(',') - .map((r) => r.trim().toLowerCase()); - if ( - memberRoles.includes('owner') || - memberRoles.includes('admin') || - memberRoles.includes('auditor') - ) { - continue; - } - - const isSuspended = suspendedEmails.has(memberEmail); - const isInactive = inactiveEmails.has(memberEmail); - const isRemoved = - !activeEmails.has(memberEmail) && !isSuspended && !isInactive; - const rampStatus: RampUserStatus | 'USER_MISSING' = isSuspended - ? 'USER_SUSPENDED' - : isInactive - ? 'USER_INACTIVE' - : isRemoved - ? 'USER_MISSING' - : 'USER_ACTIVE'; - - if (isSuspended || isInactive || isRemoved) { - try { - await db.member.update({ - where: { id: member.id }, - data: { deactivated: true, isActive: false }, - }); - results.deactivated++; - results.details.push({ - email: member.user.email, - status: 'deactivated', - reason: isSuspended - ? 'User is suspended in Ramp' - : isInactive - ? 'User is inactive in Ramp' - : 'User was removed from Ramp', - rampStatus, - }); - this.logger.log( - `Ramp deactivated member ${member.user.email} (${rampStatus})`, - ); - } catch (error) { - this.logger.error(`Error deactivating member: ${error}`); - results.errors++; - results.details.push({ - email: memberEmail, - status: 'error', - reason: `Failed to deactivate: ${error instanceof Error ? error.message : 'Unknown error'}`, - }); - } - } - } - - this.logger.log( - `Ramp sync complete: ${results.imported} imported, ${results.updated} updated, ${results.reactivated} reactivated, ${results.deactivated} deactivated, ${results.skipped} skipped, ${results.errors} errors`, - ); - - // Update lastSyncAt on the connection - await db.integrationConnection.update({ - where: { id: connectionId }, - data: { lastSyncAt: new Date() }, - }); - - const syncResult = { - success: true, - totalFound: activeUsers.length, - totalInactive: inactiveUsers.length, - totalSuspended: suspendedUsers.length, - ...results, - }; - - await this.syncLoggerService.completeLog(logId, { - imported: results.imported, - updated: results.updated, - deactivated: results.deactivated, - reactivated: results.reactivated, - skipped: results.skipped, - errors: results.errors, - totalFound: activeUsers.length, - details: results.details, - }); - - return syncResult; - } - /** * Sync employees from JumpCloud */ @@ -2013,35 +1487,6 @@ export class SyncController { }; } - /** - * Check if Ramp is connected for an organization - */ - @Post('ramp/status') - @RequirePermission('integration', 'read') - async getRampStatus(@OrganizationId() organizationId: string) { - - const connection = await this.connectionRepository.findBySlugAndOrg( - 'ramp', - organizationId, - ); - - if (!connection || connection.status !== 'active') { - return { - connected: false, - connectionId: null, - lastSyncAt: null, - nextSyncAt: null, - }; - } - - return { - connected: true, - connectionId: connection.id, - lastSyncAt: connection.lastSyncAt?.toISOString() ?? null, - nextSyncAt: connection.nextSyncAt?.toISOString() ?? null, - }; - } - /** * Get the current employee sync provider for an organization */ diff --git a/apps/api/src/integration-platform/integration-platform.module.ts b/apps/api/src/integration-platform/integration-platform.module.ts index 6277efbc5d..0a24c44859 100644 --- a/apps/api/src/integration-platform/integration-platform.module.ts +++ b/apps/api/src/integration-platform/integration-platform.module.ts @@ -10,7 +10,6 @@ import { VariablesController } from './controllers/variables.controller'; import { TaskIntegrationsController } from './controllers/task-integrations.controller'; import { WebhookController } from './controllers/webhook.controller'; import { SyncController } from './controllers/sync.controller'; -import { RampRoleMappingController } from './controllers/ramp-role-mapping.controller'; import { CredentialVaultService } from './services/credential-vault.service'; import { ConnectionService } from './services/connection.service'; import { OAuthCredentialsService } from './services/oauth-credentials.service'; @@ -27,9 +26,7 @@ import { PlatformCredentialRepository } from './repositories/platform-credential import { CheckRunRepository } from './repositories/check-run.repository'; import { DynamicIntegrationRepository } from './repositories/dynamic-integration.repository'; import { DynamicCheckRepository } from './repositories/dynamic-check.repository'; -import { RampRoleMappingService } from './services/ramp-role-mapping.service'; import { IntegrationSyncLoggerService } from './services/integration-sync-logger.service'; -import { RampApiService } from './services/ramp-api.service'; import { GenericEmployeeSyncService } from './services/generic-employee-sync.service'; @Module({ @@ -45,7 +42,6 @@ import { GenericEmployeeSyncService } from './services/generic-employee-sync.ser TaskIntegrationsController, WebhookController, SyncController, - RampRoleMappingController, ], providers: [ // Services @@ -56,9 +52,7 @@ import { GenericEmployeeSyncService } from './services/generic-employee-sync.ser OAuthTokenRevocationService, ConnectionAuthTeardownService, DynamicManifestLoaderService, - RampRoleMappingService, IntegrationSyncLoggerService, - RampApiService, GenericEmployeeSyncService, // Repositories ProviderRepository, diff --git a/apps/api/src/integration-platform/services/generic-employee-sync.service.ts b/apps/api/src/integration-platform/services/generic-employee-sync.service.ts index b66cbd6e8b..c6c4796f41 100644 --- a/apps/api/src/integration-platform/services/generic-employee-sync.service.ts +++ b/apps/api/src/integration-platform/services/generic-employee-sync.service.ts @@ -52,7 +52,7 @@ const DEFAULT_PROTECTED_ROLES = ['owner', 'admin', 'auditor']; * - Safety guards (never deactivate privileged roles) * * This extracts the common pattern from SyncController's 4 provider-specific - * implementations (Google Workspace, Rippling, JumpCloud, Ramp). + * implementations (Google Workspace, Rippling, JumpCloud). * * The provider-specific logic (fetching users, normalizing fields) is handled * by the dynamic integration's syncDefinition (DSL/code steps). diff --git a/apps/api/src/integration-platform/services/ramp-api.service.ts b/apps/api/src/integration-platform/services/ramp-api.service.ts deleted file mode 100644 index d48a14627f..0000000000 --- a/apps/api/src/integration-platform/services/ramp-api.service.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { CredentialVaultService } from './credential-vault.service'; -import { OAuthCredentialsService } from './oauth-credentials.service'; -import { - getManifest, - type RampUser, - type RampUserStatus, - type RampUsersResponse, -} from '@trycompai/integration-platform'; - -const MAX_RETRIES = 3; -const RAMP_USERS_URL = 'https://demo-api.ramp.com/developer/v1/users'; - -@Injectable() -export class RampApiService { - private readonly logger = new Logger(RampApiService.name); - - constructor( - private readonly credentialVaultService: CredentialVaultService, - private readonly oauthCredentialsService: OAuthCredentialsService, - ) {} - - /** - * Get a valid Ramp access token, refreshing if needed. - */ - async getAccessToken( - connectionId: string, - organizationId: string, - ): Promise { - let credentials = - await this.credentialVaultService.getDecryptedCredentials(connectionId); - - if (!credentials?.access_token) { - throw new HttpException( - 'No valid credentials found. Please reconnect the integration.', - HttpStatus.UNAUTHORIZED, - ); - } - - const manifest = getManifest('ramp'); - const oauthConfig = - manifest?.auth.type === 'oauth2' ? manifest.auth.config : null; - - if (oauthConfig?.supportsRefreshToken && credentials.refresh_token) { - try { - const oauthCreds = await this.oauthCredentialsService.getCredentials( - 'ramp', - organizationId, - ); - - if (oauthCreds) { - const newToken = await this.credentialVaultService.refreshOAuthTokens( - connectionId, - { - tokenUrl: oauthConfig.tokenUrl, - refreshUrl: oauthConfig.refreshUrl, - clientId: oauthCreds.clientId, - clientSecret: oauthCreds.clientSecret, - clientAuthMethod: oauthConfig.clientAuthMethod, - }, - ); - if (newToken) { - credentials = - await this.credentialVaultService.getDecryptedCredentials(connectionId); - if (!credentials?.access_token) { - throw new Error('Failed to get refreshed credentials'); - } - this.logger.log('Successfully refreshed Ramp OAuth token'); - } - } - } catch (refreshError) { - this.logger.warn( - `Token refresh failed, trying with existing token: ${refreshError}`, - ); - } - } - - if (!credentials?.access_token) { - throw new HttpException( - 'No valid credentials found. Please reconnect the integration.', - HttpStatus.UNAUTHORIZED, - ); - } - - const token = credentials.access_token; - return Array.isArray(token) ? token[0] : token; - } - - /** - * Fetch all Ramp users with pagination, retry, and rate-limit handling. - * Optionally filter by status. - */ - async fetchUsers( - accessToken: string, - status?: RampUserStatus, - ): Promise { - const users: RampUser[] = []; - let nextUrl: string | null = null; - - try { - do { - const url = nextUrl - ? new URL(nextUrl) - : new URL(RAMP_USERS_URL); - if (!nextUrl) { - url.searchParams.set('page_size', '100'); - if (status) { - url.searchParams.set('status', status); - } - } - - let response: Response | null = null; - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { - response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - }); - - if ( - response.status === 429 || - (response.status >= 500 && response.status < 600) - ) { - const retryAfter = response.headers.get('Retry-After'); - const delay = retryAfter - ? parseInt(retryAfter, 10) * 1000 - : Math.min(1000 * 2 ** attempt, 30000); - this.logger.warn( - `Ramp API returned ${response.status}, retrying in ${delay}ms (attempt ${attempt + 1}/${MAX_RETRIES})`, - ); - await new Promise((r) => setTimeout(r, delay)); - continue; - } - - break; - } - - if (!response) { - throw new HttpException( - 'Failed to fetch users from Ramp', - HttpStatus.BAD_GATEWAY, - ); - } - - if (!response.ok) { - if (response.status === 401) { - throw new HttpException( - 'Ramp credentials expired. Please reconnect.', - HttpStatus.UNAUTHORIZED, - ); - } - if (response.status === 403) { - throw new HttpException( - 'Ramp access denied. Ensure users:read scope is granted.', - HttpStatus.FORBIDDEN, - ); - } - - const errorText = await response.text(); - this.logger.error( - `Ramp API error: ${response.status} ${response.statusText} - ${errorText}`, - ); - throw new HttpException( - 'Failed to fetch users from Ramp', - HttpStatus.BAD_GATEWAY, - ); - } - - const data: RampUsersResponse = await response.json(); - if (data.data?.length) { - users.push(...data.data); - } - - nextUrl = data.page?.next ?? null; - } while (nextUrl); - } catch (error) { - if (error instanceof HttpException) throw error; - this.logger.error(`Error fetching Ramp users: ${error}`); - throw new HttpException( - 'Failed to fetch users from Ramp', - HttpStatus.BAD_GATEWAY, - ); - } - - return users; - } -} diff --git a/apps/api/src/integration-platform/services/ramp-role-mapping.service.ts b/apps/api/src/integration-platform/services/ramp-role-mapping.service.ts deleted file mode 100644 index 7e82ed05f5..0000000000 --- a/apps/api/src/integration-platform/services/ramp-role-mapping.service.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { db, type Prisma } from '@db'; -import type { RoleMappingEntry } from '@trycompai/integration-platform'; - -const BUILT_IN_ROLES = ['owner', 'admin', 'auditor', 'employee', 'contractor']; - -/** Ramp roles that map to portal-only (employee-like) access */ -const EMPLOYEE_LIKE_ROLES = new Set(['BUSINESS_USER', 'GUEST_USER']); - -/** Default Ramp → CompAI role mappings */ -const DEFAULT_BUILT_IN_MAPPINGS: Record = { - BUSINESS_OWNER: 'admin', - BUSINESS_ADMIN: 'admin', - AUDITOR: 'auditor', - BUSINESS_USER: 'employee', -}; - -/** Read-only permissions for custom roles with app access */ -const APP_READ_ONLY_PERMISSIONS: Record = { - app: ['read'], - organization: ['read'], - member: ['read'], - control: ['read'], - evidence: ['read'], - policy: ['read'], - risk: ['read'], - vendor: ['read'], - task: ['read'], - framework: ['read'], - audit: ['read'], - finding: ['read'], - questionnaire: ['read'], - integration: ['read'], - trust: ['read'], - portal: ['read', 'update'], -}; - -/** Portal-only permissions (employee-like) */ -const PORTAL_ONLY_PERMISSIONS: Record = { - policy: ['read'], - portal: ['read', 'update'], -}; - -@Injectable() -export class RampRoleMappingService { - private readonly logger = new Logger(RampRoleMappingService.name); - - /** - * Generate default mapping entries for discovered Ramp roles - */ - getDefaultMapping(rampRoles: string[]): RoleMappingEntry[] { - return rampRoles.map((rampRole) => { - const builtInMatch = DEFAULT_BUILT_IN_MAPPINGS[rampRole]; - - if (builtInMatch) { - return { - rampRole, - compRole: builtInMatch, - isBuiltIn: true, - }; - } - - // Custom role — use raw Ramp role name, determine permissions based on whether it's employee-like - const isEmployeeLike = EMPLOYEE_LIKE_ROLES.has(rampRole); - - return { - rampRole, - compRole: rampRole, - isBuiltIn: false, - permissions: isEmployeeLike - ? PORTAL_ONLY_PERMISSIONS - : APP_READ_ONLY_PERMISSIONS, - obligations: isEmployeeLike - ? { compliance: true } - : ({} as Record), - }; - }); - } - - /** - * Ensure all custom roles in the mapping exist in the database. - * Creates missing ones. - */ - async ensureCustomRolesExist( - organizationId: string, - mapping: RoleMappingEntry[], - ): Promise { - const customEntries = mapping.filter((m) => !m.isBuiltIn); - - for (const entry of customEntries) { - const existing = await db.organizationRole.findFirst({ - where: { organizationId, name: entry.compRole }, - }); - - if (existing) { - this.logger.log( - `Custom role "${entry.compRole}" already exists for org ${organizationId}`, - ); - continue; - } - - await db.organizationRole.create({ - data: { - name: entry.compRole, - permissions: JSON.stringify(entry.permissions ?? APP_READ_ONLY_PERMISSIONS), - obligations: JSON.stringify(entry.obligations ?? {}), - organizationId, - }, - }); - - this.logger.log( - `Created custom role "${entry.compRole}" for org ${organizationId}`, - ); - } - } - - /** - * Resolve a Ramp user's role to the CompAI role name using the mapping - */ - resolveRole( - rampRole: string | undefined, - mapping: RoleMappingEntry[], - ): string { - if (!rampRole) return 'employee'; - - const entry = mapping.find((m) => m.rampRole === rampRole); - if (!entry) return 'employee'; - - return entry.compRole; - } - - /** - * Get the saved role mapping from connection variables - */ - async getSavedMapping( - connectionId: string, - ): Promise { - const connection = await db.integrationConnection.findUnique({ - where: { id: connectionId }, - select: { variables: true }, - }); - - const variables = (connection?.variables ?? {}) as Record; - const mapping = variables.role_mapping; - - if (!Array.isArray(mapping) || mapping.length === 0) return null; - - return mapping as RoleMappingEntry[]; - } - - /** - * Save role mapping and discovered roles to connection variables - */ - async saveMapping( - connectionId: string, - mapping: RoleMappingEntry[], - discoveredRoles?: Array<{ role: string; userCount: number }>, - ): Promise { - const connection = await db.integrationConnection.findUnique({ - where: { id: connectionId }, - select: { variables: true }, - }); - - const existingVariables = (connection?.variables ?? {}) as Record< - string, - unknown - >; - - const updatedVariables: Record = { - ...existingVariables, - role_mapping: mapping, - }; - - if (discoveredRoles) { - updatedVariables.discovered_roles = discoveredRoles; - } - - await db.integrationConnection.update({ - where: { id: connectionId }, - data: { - variables: updatedVariables as unknown as Prisma.InputJsonValue, - }, - }); - } - - /** - * Save only discovered roles without touching the role_mapping field - */ - async saveDiscoveredRoles( - connectionId: string, - discoveredRoles: Array<{ role: string; userCount: number }>, - ): Promise { - const connection = await db.integrationConnection.findUnique({ - where: { id: connectionId }, - select: { variables: true }, - }); - - const existingVariables = (connection?.variables ?? {}) as Record< - string, - unknown - >; - - const updatedVariables: Record = { - ...existingVariables, - discovered_roles: discoveredRoles, - }; - - await db.integrationConnection.update({ - where: { id: connectionId }, - data: { - variables: updatedVariables as unknown as Prisma.InputJsonValue, - }, - }); - } - - /** - * Get cached discovered roles from connection variables - */ - async getCachedDiscoveredRoles( - connectionId: string, - ): Promise | null> { - const connection = await db.integrationConnection.findUnique({ - where: { id: connectionId }, - select: { variables: true }, - }); - - const variables = (connection?.variables ?? {}) as Record; - const roles = variables.discovered_roles; - - if (!Array.isArray(roles) || roles.length === 0) return null; - - return roles as Array<{ role: string; userCount: number }>; - } - -} diff --git a/apps/api/src/trigger/integration-platform/sync-employees-schedule.ts b/apps/api/src/trigger/integration-platform/sync-employees-schedule.ts index 11b9fa5e8e..4bfe108f62 100644 --- a/apps/api/src/trigger/integration-platform/sync-employees-schedule.ts +++ b/apps/api/src/trigger/integration-platform/sync-employees-schedule.ts @@ -197,9 +197,6 @@ async function syncProvider(params: SyncProviderParams): Promise { case 'jumpcloud': return syncJumpCloud({ connectionId, organizationId }); - case 'ramp': - return syncRamp({ connectionId, organizationId }); - default: // Try generic dynamic sync endpoint for non-built-in providers return syncDynamicProvider({ providerSlug, connectionId, organizationId }); @@ -319,41 +316,6 @@ async function syncJumpCloud({ }; } -async function syncRamp({ - connectionId, - organizationId, -}: { - connectionId: string; - organizationId: string; -}): Promise { - const url = new URL(`${API_BASE_URL}/v1/integrations/sync/ramp/employees`); - url.searchParams.set('connectionId', connectionId); - - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-service-token': process.env.SERVICE_TOKEN_TRIGGER!, - 'x-organization-id': organizationId, - }, - }); - - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`Ramp sync failed: ${response.status} - ${errorBody}`); - } - - const data = await response.json(); - return { - success: data.success, - imported: data.imported || 0, - reactivated: data.reactivated || 0, - deactivated: data.deactivated || 0, - skipped: data.skipped || 0, - errors: data.errors || 0, - }; -} - async function syncDynamicProvider({ providerSlug, connectionId, diff --git a/apps/app/src/app/(app)/[orgId]/integrations/data/categories/hr.ts b/apps/app/src/app/(app)/[orgId]/integrations/data/categories/hr.ts index 2d2f171980..1d6e7df454 100644 --- a/apps/app/src/app/(app)/[orgId]/integrations/data/categories/hr.ts +++ b/apps/app/src/app/(app)/[orgId]/integrations/data/categories/hr.ts @@ -43,19 +43,6 @@ export const hrIntegrations: Integration[] = [ ], setupHint: 'Requires Rippling API credentials', }, - { - id: 'ramp', - name: 'Ramp', - domain: 'ramp.com', - description: 'Corporate cards and expense management with employee directory sync', - category: 'HR & People', - examplePrompts: [ - 'Sync employees from Ramp', - 'Check active Ramp cardholders', - 'Verify employee status from Ramp', - ], - setupHint: 'Requires Ramp OAuth app with users:read scope', - }, { id: 'gusto', name: 'Gusto', diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/RampRoleMappingSheet.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/RampRoleMappingSheet.tsx deleted file mode 100644 index f99be043ed..0000000000 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/RampRoleMappingSheet.tsx +++ /dev/null @@ -1,147 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { Button } from '@trycompai/design-system'; -import { Close } from '@trycompai/design-system/icons'; -import { apiClient } from '@/lib/api-client'; -import { toast } from 'sonner'; -import { RampRoleMappingRow } from '../../role-mapping/components/RampRoleMappingRow'; -import type { RoleMappingEntry } from '../../role-mapping/components/RampRoleMappingRow'; -import type { RoleMappingData } from '../hooks/useEmployeeSync'; - -interface RampRoleMappingSheetProps { - open: boolean; - onOpenChange: (open: boolean) => void; - organizationId: string; - data: RoleMappingData; - onSaved: () => void; -} - -export function RampRoleMappingSheet({ - open, - onOpenChange, - organizationId, - data, - onSaved, -}: RampRoleMappingSheetProps) { - const initialMapping = data.existingMapping ?? data.defaultMapping; - const [mapping, setMapping] = useState(initialMapping); - const [isSaving, setIsSaving] = useState(false); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - const handleEntryChange = (index: number, updated: RoleMappingEntry) => { - setMapping((prev) => { - const next = [...prev]; - next[index] = updated; - return next; - }); - }; - - const handleSave = async () => { - setIsSaving(true); - try { - const response = await apiClient.post( - `/v1/integrations/sync/ramp/role-mapping?organizationId=${organizationId}`, - { connectionId: data.connectionId, mapping }, - ); - - if (response.error) { - toast.error(response.error); - return; - } - - toast.success('Role mapping saved'); - onSaved(); - } catch { - toast.error('Failed to save role mapping'); - } finally { - setIsSaving(false); - } - }; - - if (!open || !mounted) return null; - - return createPortal( - <> - {/* Backdrop */} -
onOpenChange(false)} - /> - - {/* Dialog */} -
-
e.stopPropagation()} - > - {/* Header */} -
-
-

- Configure Ramp Role Mapping -

-

- Map Ramp roles to your organization's roles. Custom roles - will be created automatically with default permissions you can - customize. -

-
- -
- - {/* Column headers */} -
-

- Ramp Role -

-
-

- Comp AI Role -

-
- - {/* Mapping rows */} -
-
- {mapping.map((entry, index) => ( - handleEntryChange(index, updated)} - /> - ))} -
-
- - {/* Footer */} -
- - -
-
-
- , - document.body, - ); -} diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index 0436678794..688c4d12a9 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -39,7 +39,6 @@ import { Search } from '@trycompai/design-system/icons'; import { apiClient } from '@/lib/api-client'; import { MemberRow } from './MemberRow'; import { PendingInvitationRow } from './PendingInvitationRow'; -import { RampRoleMappingSheet } from './RampRoleMappingSheet'; import type { MemberWithUser, TeamMembersData } from './TeamMembers'; import type { EmployeeSyncConnectionsData } from '../data/queries'; @@ -109,18 +108,12 @@ export function TeamMembersClient({ googleWorkspaceConnectionId, ripplingConnectionId, jumpcloudConnectionId, - rampConnectionId, selectedProvider, isSyncing, syncEmployees, hasAnyConnection, getProviderName, getProviderLogo, - showRoleMappingSheet, - roleMappingData, - handleRoleMappingClose, - handleRoleMappingSaved, - openRoleMappingEditor, availableProviders, } = useEmployeeSync({ organizationId, initialData: employeeSyncData }); @@ -354,7 +347,7 @@ export function TeamMembersClient({ onValueChange={(value) => { if (value) { handleEmployeeSync( - value as 'google-workspace' | 'rippling' | 'jumpcloud' | 'ramp', + value as 'google-workspace' | 'rippling' | 'jumpcloud', ); } }} @@ -457,27 +450,9 @@ export function TeamMembersClient({
)} - {rampConnectionId && ( - -
- Ramp - Ramp - {selectedProvider === 'ramp' && ( - Active - )} -
-
- )} {/* Dynamic sync providers (from dynamic integrations) */} {availableProviders - .filter((p) => p.connected && !['google-workspace', 'rippling', 'jumpcloud', 'ramp'].includes(p.slug)) + .filter((p) => p.connected && !['google-workspace', 'rippling', 'jumpcloud'].includes(p.slug)) .map((provider) => (
@@ -574,17 +549,6 @@ export function TeamMembersClient({ )} - {roleMappingData && ( - { - if (!open) handleRoleMappingClose(); - }} - organizationId={organizationId} - data={roleMappingData} - onSaved={handleRoleMappingSaved} - /> - )} ); } diff --git a/apps/app/src/app/(app)/[orgId]/people/all/data/queries.ts b/apps/app/src/app/(app)/[orgId]/people/all/data/queries.ts index a964403da2..c81a901bbc 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/data/queries.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/data/queries.ts @@ -14,7 +14,6 @@ export interface EmployeeSyncConnectionsData { googleWorkspaceConnectionId: string | null; ripplingConnectionId: string | null; jumpcloudConnectionId: string | null; - rampConnectionId: string | null; selectedProvider: string | null | undefined; lastSyncAt: Date | null; nextSyncAt: Date | null; @@ -32,7 +31,7 @@ interface ConnectionStatus { export async function getEmployeeSyncConnections( organizationId: string, ): Promise { - const [gwResponse, ripplingResponse, jumpcloudResponse, rampResponse, providerResponse, availableResponse] = + const [gwResponse, ripplingResponse, jumpcloudResponse, providerResponse, availableResponse] = await Promise.all([ serverApi.post( `/v1/integrations/sync/google-workspace/status?organizationId=${organizationId}`, @@ -43,9 +42,6 @@ export async function getEmployeeSyncConnections( serverApi.post( `/v1/integrations/sync/jumpcloud/status?organizationId=${organizationId}`, ), - serverApi.post( - `/v1/integrations/sync/ramp/status?organizationId=${organizationId}`, - ), serverApi.get<{ provider: string | null }>( `/v1/integrations/sync/employee-sync-provider?organizationId=${organizationId}`, ), @@ -67,8 +63,6 @@ export async function getEmployeeSyncConnections( selectedSyncTimes = ripplingResponse.data ?? null; } else if (selectedProviderSlug === 'jumpcloud') { selectedSyncTimes = jumpcloudResponse.data ?? null; - } else if (selectedProviderSlug === 'ramp') { - selectedSyncTimes = rampResponse.data ?? null; } else if (selectedProviderSlug) { // Dynamic provider — get sync times from available-providers data const dynProvider = availableProviders.find((p) => p.slug === selectedProviderSlug); @@ -90,10 +84,6 @@ export async function getEmployeeSyncConnections( jumpcloudResponse.data?.connected && jumpcloudResponse.data.connectionId ? jumpcloudResponse.data.connectionId : null, - rampConnectionId: - rampResponse.data?.connected && rampResponse.data.connectionId - ? rampResponse.data.connectionId - : null, selectedProvider: selectedProviderSlug, lastSyncAt: selectedSyncTimes?.lastSyncAt ? new Date(selectedSyncTimes.lastSyncAt) : null, nextSyncAt: selectedSyncTimes?.nextSyncAt ? new Date(selectedSyncTimes.nextSyncAt) : null, diff --git a/apps/app/src/app/(app)/[orgId]/people/all/hooks/useEmployeeSync.ts b/apps/app/src/app/(app)/[orgId]/people/all/hooks/useEmployeeSync.ts index 887971e01c..d7fa5e5922 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/hooks/useEmployeeSync.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/hooks/useEmployeeSync.ts @@ -7,12 +7,11 @@ import useSWR from 'swr'; import type { EmployeeSyncConnectionsData, SyncProviderInfo } from '../data/queries'; -type BuiltInSyncProvider = 'google-workspace' | 'rippling' | 'jumpcloud' | 'ramp'; +type BuiltInSyncProvider = 'google-workspace' | 'rippling' | 'jumpcloud'; type SyncProvider = string; interface SyncResult { success: boolean; - requiresRoleMapping?: boolean; totalFound: number; imported: number; updated: number; @@ -22,31 +21,6 @@ interface SyncResult { errors: number; } -interface DiscoveredRole { - role: string; - userCount: number; -} - -interface RoleMappingEntry { - rampRole: string; - compRole: string; - isBuiltIn: boolean; - permissions?: Record; - obligations?: Record; -} - -export interface RoleMappingData { - discoveredRoles: DiscoveredRole[]; - defaultMapping: RoleMappingEntry[]; - existingMapping: RoleMappingEntry[] | null; - existingCustomRoles: Array<{ - name: string; - permissions: Record; - obligations: Record; - }>; - connectionId: string; -} - interface UseEmployeeSyncOptions { organizationId: string; initialData: EmployeeSyncConnectionsData; @@ -56,7 +30,6 @@ interface UseEmployeeSyncReturn { googleWorkspaceConnectionId: string | null; ripplingConnectionId: string | null; jumpcloudConnectionId: string | null; - rampConnectionId: string | null; selectedProvider: SyncProvider | null; isSyncing: boolean; syncEmployees: (provider: SyncProvider) => Promise; @@ -64,11 +37,6 @@ interface UseEmployeeSyncReturn { hasAnyConnection: boolean; getProviderName: (provider: SyncProvider) => string; getProviderLogo: (provider: SyncProvider) => string; - showRoleMappingSheet: boolean; - roleMappingData: RoleMappingData | null; - handleRoleMappingClose: () => void; - handleRoleMappingSaved: () => void; - openRoleMappingEditor: () => Promise; /** All available sync providers (built-in + dynamic) */ availableProviders: SyncProviderInfo[]; } @@ -77,7 +45,6 @@ const BUILT_IN_PROVIDERS = new Set([ 'google-workspace', 'rippling', 'jumpcloud', - 'ramp', ]); const PROVIDER_CONFIG = { @@ -96,11 +63,6 @@ const PROVIDER_CONFIG = { shortName: 'JumpCloud', logo: 'https://img.logo.dev/jumpcloud.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ&format=png&retina=true', }, - ramp: { - name: 'Ramp', - shortName: 'Ramp', - logo: 'https://img.logo.dev/ramp.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ&format=png&retina=true', - }, } as const; export const useEmployeeSync = ({ @@ -108,9 +70,6 @@ export const useEmployeeSync = ({ initialData, }: UseEmployeeSyncOptions): UseEmployeeSyncReturn => { const [isSyncing, setIsSyncing] = useState(false); - const [showRoleMappingSheet, setShowRoleMappingSheet] = useState(false); - const [roleMappingData, setRoleMappingData] = useState(null); - const [pendingSyncProvider, setPendingSyncProvider] = useState(null); const { data, mutate } = useSWR( ['employee-sync-connections', organizationId], @@ -121,7 +80,6 @@ export const useEmployeeSync = ({ const googleWorkspaceConnectionId = data?.googleWorkspaceConnectionId ?? null; const ripplingConnectionId = data?.ripplingConnectionId ?? null; const jumpcloudConnectionId = data?.jumpcloudConnectionId ?? null; - const rampConnectionId = data?.rampConnectionId ?? null; const selectedProvider = data?.selectedProvider ?? null; const setSyncProvider = async (provider: SyncProvider | null) => { @@ -152,7 +110,6 @@ export const useEmployeeSync = ({ if (provider === 'google-workspace') return googleWorkspaceConnectionId; if (provider === 'rippling') return ripplingConnectionId; if (provider === 'jumpcloud') return jumpcloudConnectionId; - if (provider === 'ramp') return rampConnectionId; // Dynamic provider — look up from availableProviders const dynProvider = availableProviders.find((p) => p.slug === provider); return dynProvider?.connectionId ?? null; @@ -187,36 +144,6 @@ export const useEmployeeSync = ({ getSyncUrl(provider, connectionId), ); - // Handle role mapping requirement (Ramp only) - if (response.data?.requiresRoleMapping && provider === 'ramp' && connectionId) { - setPendingSyncProvider(provider); - try { - const discoverResponse = await apiClient.post<{ - discoveredRoles: DiscoveredRole[]; - defaultMapping: RoleMappingEntry[]; - existingMapping: RoleMappingEntry[] | null; - existingCustomRoles: Array<{ - name: string; - permissions: Record; - obligations: Record; - }>; - }>( - `/v1/integrations/sync/ramp/discover-roles?organizationId=${organizationId}&connectionId=${connectionId}`, - ); - - if (discoverResponse.data) { - setRoleMappingData({ - ...discoverResponse.data, - connectionId, - }); - setShowRoleMappingSheet(true); - } - } catch { - toast.error('Failed to discover Ramp roles'); - } - return null; - } - if (response.data?.success) { const { imported, updated, reactivated, deactivated, skipped, errors } = response.data; @@ -272,62 +199,10 @@ export const useEmployeeSync = ({ return dynProvider?.logoUrl ?? ''; }; - const openRoleMappingEditor = async () => { - if (!rampConnectionId) { - toast.error('Ramp is not connected'); - return; - } - - try { - const discoverResponse = await apiClient.post<{ - discoveredRoles: DiscoveredRole[]; - defaultMapping: RoleMappingEntry[]; - existingMapping: RoleMappingEntry[] | null; - existingCustomRoles: Array<{ - name: string; - permissions: Record; - obligations: Record; - }>; - }>( - `/v1/integrations/sync/ramp/discover-roles?organizationId=${organizationId}&connectionId=${rampConnectionId}`, - ); - - if (discoverResponse.data) { - setRoleMappingData({ - ...discoverResponse.data, - connectionId: rampConnectionId, - }); - setShowRoleMappingSheet(true); - } - } catch { - toast.error('Failed to load role mapping'); - } - }; - - const handleRoleMappingClose = () => { - setShowRoleMappingSheet(false); - setRoleMappingData(null); - setPendingSyncProvider(null); - setIsSyncing(false); - }; - - const handleRoleMappingSaved = () => { - setShowRoleMappingSheet(false); - setRoleMappingData(null); - - // Retry sync with the pending provider now that mapping is saved - if (pendingSyncProvider) { - const provider = pendingSyncProvider; - setPendingSyncProvider(null); - syncEmployees(provider); - } - }; - const hasAnyBuiltInConnection = !!( googleWorkspaceConnectionId || ripplingConnectionId || - jumpcloudConnectionId || - rampConnectionId + jumpcloudConnectionId ); const hasDynamicConnection = availableProviders.some( (p) => p.connected && !BUILT_IN_PROVIDERS.has(p.slug), @@ -337,7 +212,6 @@ export const useEmployeeSync = ({ googleWorkspaceConnectionId, ripplingConnectionId, jumpcloudConnectionId, - rampConnectionId, selectedProvider, isSyncing, syncEmployees, @@ -345,11 +219,6 @@ export const useEmployeeSync = ({ hasAnyConnection: hasAnyBuiltInConnection || hasDynamicConnection, getProviderName, getProviderLogo, - showRoleMappingSheet, - roleMappingData, - handleRoleMappingClose, - handleRoleMappingSaved, - openRoleMappingEditor, availableProviders, }; }; diff --git a/apps/app/src/app/(app)/[orgId]/people/page.tsx b/apps/app/src/app/(app)/[orgId]/people/page.tsx index 06efcae506..75f12ca4d5 100644 --- a/apps/app/src/app/(app)/[orgId]/people/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/page.tsx @@ -10,7 +10,6 @@ import { redirect } from 'next/navigation'; import { TeamMembers } from './all/components/TeamMembers'; import { getEmployeeSyncConnections } from './all/data/queries'; import { PeoplePageTabs } from './components/PeoplePageTabs'; -import { RoleMappingTab } from './role-mapping/components/RoleMappingTab'; import { EmployeesOverview } from './dashboard/components/EmployeesOverview'; import { DeviceComplianceChart } from './devices/components/DeviceComplianceChart'; import { DeviceAgentDevicesList } from './devices/components/DeviceAgentDevicesList'; @@ -186,15 +185,8 @@ export default async function PeoplePage({ params }: { params: Promise<{ orgId: members={membersWithUsers} /> } - showRoleMapping={!!syncConnections?.rampConnectionId} - roleMappingContent={ - syncConnections?.rampConnectionId ? ( - - ) : null - } + showRoleMapping={false} + roleMappingContent={null} showEmployeeTasks={showEmployeeTasks} canInviteUsers={canInviteUsers} canManageMembers={canManageMembers} diff --git a/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingContent.tsx b/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingContent.tsx deleted file mode 100644 index fad2b4f12c..0000000000 --- a/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingContent.tsx +++ /dev/null @@ -1,195 +0,0 @@ -'use client'; - -import { useEffect, useRef, useState } from 'react'; -import { Button } from '@trycompai/design-system'; -import { apiClient } from '@/lib/api-client'; -import { toast } from 'sonner'; -import { RampRoleMappingRow } from './RampRoleMappingRow'; -import type { RoleMappingEntry } from './RampRoleMappingRow'; - -interface DiscoveredRole { - role: string; - userCount: number; -} - -interface DiscoverRolesResponse { - discoveredRoles: DiscoveredRole[]; - defaultMapping: RoleMappingEntry[]; - existingMapping: RoleMappingEntry[] | null; - existingCustomRoles: Array<{ - name: string; - permissions: Record; - obligations: Record; - }>; -} - -interface RampRoleMappingContentProps { - organizationId: string; - connectionId: string; -} - -export function RampRoleMappingContent({ - organizationId, - connectionId, -}: RampRoleMappingContentProps) { - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [mapping, setMapping] = useState([]); - const savedMappingRef = useRef(''); - const [discoveredRoles, setDiscoveredRoles] = useState([]); - const [existingCustomRoles, setExistingCustomRoles] = useState< - Array<{ - name: string; - permissions: Record; - obligations: Record; - }> - >([]); - - const fetchRoles = async (refresh = false) => { - try { - const refreshParam = refresh ? '&refresh=true' : ''; - const response = await apiClient.post( - `/v1/integrations/sync/ramp/discover-roles?organizationId=${organizationId}&connectionId=${connectionId}${refreshParam}`, - ); - - if (response.data) { - const initialMapping = - response.data.existingMapping ?? response.data.defaultMapping; - setDiscoveredRoles(response.data.discoveredRoles); - setExistingCustomRoles(response.data.existingCustomRoles); - setMapping(initialMapping); - savedMappingRef.current = JSON.stringify(initialMapping); - } - } catch (error) { - toast.error('Failed to load Ramp roles'); - throw error; - } - }; - - useEffect(() => { - const load = async () => { - setIsLoading(true); - try { - await fetchRoles(); - } catch { - // error toast already shown by fetchRoles - } finally { - setIsLoading(false); - } - }; - load(); - }, [organizationId, connectionId]); - - const handleRefresh = async () => { - setIsRefreshing(true); - try { - await fetchRoles(true); - toast.success('Roles refreshed from Ramp'); - } catch { - // fetchRoles already shows error toast - } finally { - setIsRefreshing(false); - } - }; - - const handleEntryChange = (index: number, updated: RoleMappingEntry) => { - setMapping((prev) => { - const next = [...prev]; - next[index] = updated; - return next; - }); - }; - - const handleSave = async () => { - setIsSaving(true); - try { - const response = await apiClient.post( - `/v1/integrations/sync/ramp/role-mapping?organizationId=${organizationId}`, - { connectionId, mapping }, - ); - - if (response.error) { - toast.error(response.error); - return; - } - - toast.success('Role mapping saved'); - savedMappingRef.current = JSON.stringify(mapping); - } catch { - toast.error('Failed to save role mapping'); - } finally { - setIsSaving(false); - } - }; - - const isDirty = JSON.stringify(mapping) !== savedMappingRef.current; - - if (isLoading) { - return ( -
- {[1, 2, 3].map((i) => ( -
- ))} -
- ); - } - - if (mapping.length === 0) { - return ( -
- No roles discovered from Ramp. Try syncing employees first. -
- ); - } - - return ( -
- {/* Column headers */} -
-

- Ramp Role -

-
-

- Comp AI Role -

-
- - {/* Mapping rows */} -
- {mapping.map((entry, index) => ( - handleEntryChange(index, updated)} - /> - ))} -
- - {/* Info note */} -

- Members with Owner, Admin, or Auditor roles in Comp AI are not - affected by sync — their roles are preserved. -

- - {/* Actions */} -
- - -
-
- ); -} diff --git a/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingRow.tsx b/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingRow.tsx deleted file mode 100644 index 61dc465f28..0000000000 --- a/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RampRoleMappingRow.tsx +++ /dev/null @@ -1,233 +0,0 @@ -'use client'; - -import { useState, useRef } from 'react'; -import { ChevronDown, Settings } from '@trycompai/design-system/icons'; -import { PermissionMatrix } from '../../../settings/roles/components/PermissionMatrix'; - -const BUILT_IN_ROLES = [ - { value: 'admin', label: 'Admin' }, - { value: 'auditor', label: 'Auditor' }, - { value: 'employee', label: 'Employee' }, - { value: 'contractor', label: 'Contractor' }, -]; - -export interface RoleMappingEntry { - rampRole: string; - compRole: string; - isBuiltIn: boolean; - permissions?: Record; - obligations?: Record; -} - -interface RampRoleMappingRowProps { - entry: RoleMappingEntry; - existingCustomRoles: Array<{ - name: string; - permissions: Record; - obligations: Record; - }>; - onChange: (updated: RoleMappingEntry) => void; -} - -export function RampRoleMappingRow({ - entry, - existingCustomRoles, - onChange, -}: RampRoleMappingRowProps) { - const [expanded, setExpanded] = useState(false); - const [dropdownOpen, setDropdownOpen] = useState(false); - const [customInput, setCustomInput] = useState(''); - const inputRef = useRef(null); - const dropdownRef = useRef(null); - - const displayValue = entry.isBuiltIn - ? BUILT_IN_ROLES.find((r) => r.value === entry.compRole)?.label ?? - entry.compRole - : entry.compRole; - - const handleSelectBuiltIn = (value: string) => { - onChange({ - ...entry, - compRole: value, - isBuiltIn: true, - permissions: undefined, - obligations: undefined, - }); - setDropdownOpen(false); - setCustomInput(''); - }; - - const handleCustomSubmit = () => { - const val = customInput.trim(); - if (!val) return; - - // Check if this matches an existing custom role — use its permissions - const existingRole = existingCustomRoles.find((r) => r.name === val); - - onChange({ - ...entry, - compRole: val, - isBuiltIn: false, - permissions: existingRole?.permissions ?? { - policy: ['read'], - portal: ['read', 'update'], - }, - obligations: existingRole?.obligations ?? {}, - }); - setDropdownOpen(false); - setCustomInput(''); - }; - - return ( -
- {/* Two-column mapping row */} -
- {/* Left: Ramp role */} -
-

{entry.rampRole}

- {!entry.isBuiltIn && ( - - )} -
- - {/* Arrow */} - - - {/* Right: Custom select with inline input */} -
- - - {dropdownOpen && ( -
- {BUILT_IN_ROLES.map((role) => ( - - ))} - - {/* Existing custom roles */} - {existingCustomRoles.length > 0 && ( - <> -
-

Custom roles

-
- {existingCustomRoles.map((role) => ( - - ))} - - )} - - {/* New custom role input */} -
-
- setCustomInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleCustomSubmit(); - } - if (e.key === 'Escape') { - setDropdownOpen(false); - setCustomInput(''); - } - }} - placeholder="Custom role name..." - className="flex-1 text-sm px-2 py-1.5 rounded border outline-none focus:ring-1 focus:ring-foreground/20 bg-background" - /> - -
-
-
- )} -
-
- - {/* Expanded permissions for custom roles */} - {!entry.isBuiltIn && expanded && ( -
- - onChange({ ...entry, permissions }) - } - obligations={entry.obligations} - onObligationsChange={(obligations) => - onChange({ ...entry, obligations }) - } - /> -
- )} -
- ); -} diff --git a/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RoleMappingTab.tsx b/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RoleMappingTab.tsx deleted file mode 100644 index f4da227048..0000000000 --- a/apps/app/src/app/(app)/[orgId]/people/role-mapping/components/RoleMappingTab.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { - Empty, - EmptyDescription, - EmptyHeader, - EmptyTitle, - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from '@trycompai/design-system'; -import { RampRoleMappingContent } from './RampRoleMappingContent'; - -const PROVIDER_LOGOS = { - ramp: 'https://img.logo.dev/ramp.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ&format=png&retina=true', -} as const; - -interface RoleMappingTabProps { - organizationId: string; - rampConnectionId: string | null; -} - -export function RoleMappingTab({ - organizationId, - rampConnectionId, -}: RoleMappingTabProps) { - if (!rampConnectionId) { - return ( - - - No sync providers connected - - Connect a provider like Ramp in Integrations to configure role - mapping. - - - - ); - } - - return ( - -
- - - Ramp - Ramp - - -
- - - - -
- ); -} diff --git a/packages/db/prisma/schema/integration-sync-log.prisma b/packages/db/prisma/schema/integration-sync-log.prisma index 2558dca999..5cce4660a4 100644 --- a/packages/db/prisma/schema/integration-sync-log.prisma +++ b/packages/db/prisma/schema/integration-sync-log.prisma @@ -8,7 +8,7 @@ model IntegrationSyncLog { organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - /// Provider slug (e.g., "ramp", "google-workspace", "rippling", "jumpcloud") + /// Provider slug (e.g., "google-workspace", "rippling", "jumpcloud") provider String /// Event type (e.g., "employee_sync", "role_discovery", "role_mapping_save") eventType String diff --git a/packages/integration-platform/src/index.ts b/packages/integration-platform/src/index.ts index e914650883..5f8c68a2ac 100644 --- a/packages/integration-platform/src/index.ts +++ b/packages/integration-platform/src/index.ts @@ -130,17 +130,6 @@ export { manifest as githubManifest } from './manifests/github'; // Directory sync email include/exclude terms (Google Workspace, JumpCloud, checks) export { matchesSyncFilterTerms, parseSyncFilterTerms } from './sync-filter/email-exclusion-terms'; -// Ramp types (used by sync controller) -export type { - RampUser, - RampUserStatus, - RampUserRole, - RampKnownRole, - RampEmployee, - RampUsersResponse, - RoleMappingEntry, -} from './manifests/ramp/types'; - // API Response types (for frontend and API type sharing) export type { CheckRunFindingResponse, diff --git a/packages/integration-platform/src/manifests/ramp/checks/employee-sync.ts b/packages/integration-platform/src/manifests/ramp/checks/employee-sync.ts deleted file mode 100644 index cc57c9db35..0000000000 --- a/packages/integration-platform/src/manifests/ramp/checks/employee-sync.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { TASK_TEMPLATES } from '../../../task-mappings'; -import type { CheckContext, IntegrationCheck } from '../../../types'; -import type { - RampUser, - RampEmployee, - RampUserStatus, - RampUsersResponse, -} from '../types'; - -const getUserStatus = ( - user: RampUser, -): RampEmployee['status'] => { - switch (user.status) { - case 'USER_ACTIVE': - return 'active'; - case 'USER_INACTIVE': - return 'inactive'; - case 'USER_SUSPENDED': - return 'suspended'; - case 'USER_ONBOARDING': - return 'onboarding'; - case 'INVITE_PENDING': - return 'invite_pending'; - case 'INVITE_EXPIRED': - return 'invite_expired'; - default: - return 'inactive'; - } -}; - -const getFullName = (user: RampUser): string => { - const parts = [user.first_name, user.last_name].filter(Boolean); - if (parts.length > 0) return parts.join(' '); - return user.email?.split('@')[0] ?? 'Unknown'; -}; - -/** - * Employee Sync Check - * - * Fetches all users from Ramp and provides evidence for audit trail. - * This gives auditors a complete snapshot of the employee roster. - */ -export const employeeSyncCheck: IntegrationCheck = { - id: 'employee-sync', - name: 'Employee Sync', - description: - 'Sync users from Ramp as employees for access review and verification', - taskMapping: TASK_TEMPLATES.employeeAccess, - defaultSeverity: 'info', - - run: async (ctx: CheckContext) => { - ctx.log('Starting Ramp Employee Sync'); - - const allUsers: RampUser[] = []; - - // Fetch active + inactive users (default behavior) - ctx.log('Fetching users from Ramp...'); - - const fetchAllRampUsers = async ( - initialPath: string, - ): Promise => { - const result: RampUser[] = []; - let currentUrl: string | null = initialPath; - let isFirst = true; - - while (currentUrl) { - const response: RampUsersResponse = await ctx.fetch( - currentUrl, - isFirst ? { baseUrl: 'https://demo-api.ramp.com' } : undefined, - ); - isFirst = false; - - if (response.data?.length) { - result.push(...response.data); - } - - currentUrl = response.page?.next ?? null; - } - - return result; - }; - - const baseUsers = await fetchAllRampUsers( - '/developer/v1/users?page_size=100', - ); - allUsers.push(...baseUsers); - ctx.log(`Fetched ${baseUsers.length} active/inactive users`); - - // Also fetch suspended users (not included by default) - const suspendedUsers = await fetchAllRampUsers( - '/developer/v1/users?page_size=100&status=USER_SUSPENDED', - ); - allUsers.push(...suspendedUsers); - ctx.log(`Fetched ${suspendedUsers.length} suspended users`); - - ctx.log(`Fetched ${allUsers.length} total users from Ramp`); - - // Filter out non-syncable statuses - const syncableStatuses = new Set([ - 'USER_ACTIVE', - 'USER_INACTIVE', - 'USER_SUSPENDED', - ]); - const syncableUsers = allUsers.filter( - (u) => u.status && syncableStatuses.has(u.status), - ); - - // Transform to employee format - const employees: RampEmployee[] = syncableUsers.map((user) => ({ - id: user.id, - email: user.email, - name: getFullName(user), - firstName: user.first_name, - lastName: user.last_name, - employeeId: user.employee_id, - status: getUserStatus(user), - role: user.role, - departmentId: user.department_id, - locationId: user.location_id, - managerId: user.manager_id, - phone: user.phone, - isManager: user.is_manager, - })); - - // Calculate statistics - const activeEmployees = employees.filter((e) => e.status === 'active'); - const inactiveEmployees = employees.filter((e) => e.status === 'inactive'); - const suspendedEmployees = employees.filter( - (e) => e.status === 'suspended', - ); - const managers = employees.filter((e) => e.isManager); - - // Group by role for summary - const roleCounts = new Map(); - for (const emp of employees) { - const role = emp.role || 'Unknown'; - roleCounts.set(role, (roleCounts.get(role) || 0) + 1); - } - - const roleSummary = Array.from(roleCounts.entries()) - .map(([role, count]) => ({ role, count })) - .sort((a, b) => b.count - a.count); - - ctx.pass({ - title: 'Ramp Employee List', - resourceType: 'organization', - resourceId: 'ramp', - description: `Retrieved ${employees.length} employees from Ramp (${activeEmployees.length} active, ${inactiveEmployees.length} inactive, ${suspendedEmployees.length} suspended, ${managers.length} managers)`, - evidence: { - totalUsers: employees.length, - activeCount: activeEmployees.length, - inactiveCount: inactiveEmployees.length, - suspendedCount: suspendedEmployees.length, - managerCount: managers.length, - roleSummary, - reviewedAt: new Date().toISOString(), - employees, - }, - }); - - ctx.log('Ramp Employee Sync complete'); - }, -}; diff --git a/packages/integration-platform/src/manifests/ramp/checks/index.ts b/packages/integration-platform/src/manifests/ramp/checks/index.ts deleted file mode 100644 index 890c2d48fc..0000000000 --- a/packages/integration-platform/src/manifests/ramp/checks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { employeeSyncCheck } from './employee-sync'; diff --git a/packages/integration-platform/src/manifests/ramp/index.ts b/packages/integration-platform/src/manifests/ramp/index.ts deleted file mode 100644 index 2e7b140efc..0000000000 --- a/packages/integration-platform/src/manifests/ramp/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Ramp Integration Manifest - * - * This integration connects to Ramp to sync employee data. - * It uses OAuth2 and the Developer API user endpoints. - */ - -import type { IntegrationManifest } from '../../types'; -import { employeeSyncCheck } from './checks'; - -export const rampManifest: IntegrationManifest = { - id: 'ramp', - name: 'Ramp', - description: 'Sync employees from Ramp to your organization members', - category: 'HR & People', - logoUrl: 'https://img.logo.dev/ramp.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ', - docsUrl: 'https://docs.ramp.com/developer-api/v1/authorization', - isActive: true, - - auth: { - type: 'oauth2', - config: { - authorizeUrl: 'https://demo.ramp.com/v1/authorize', - tokenUrl: 'https://demo-api.ramp.com/developer/v1/token', - scopes: ['users:read'], - pkce: false, - clientAuthMethod: 'header', - supportsRefreshToken: true, - revoke: { - url: 'https://demo-api.ramp.com/developer/v1/token/revoke', - method: 'POST', - auth: 'basic', - body: 'form', - tokenField: 'token', - }, - setupInstructions: `To create a Ramp OAuth app: -1. Open the Ramp Developer Dashboard -2. Create a new OAuth app -3. Add the callback URL shown below -4. Request the users:read scope -5. Copy the Client ID and Client Secret`, - }, - }, - - baseUrl: 'https://demo-api.ramp.com', - defaultHeaders: { - Accept: 'application/json', - }, - - capabilities: ['sync', 'checks'], - checks: [employeeSyncCheck], -}; - -export default rampManifest; -export * from './types'; diff --git a/packages/integration-platform/src/manifests/ramp/types.ts b/packages/integration-platform/src/manifests/ramp/types.ts deleted file mode 100644 index 72e032b708..0000000000 --- a/packages/integration-platform/src/manifests/ramp/types.ts +++ /dev/null @@ -1,70 +0,0 @@ -export type RampUserStatus = - | 'USER_ACTIVE' - | 'USER_INACTIVE' - | 'USER_SUSPENDED' - | 'INVITE_PENDING' - | 'INVITE_EXPIRED' - | 'USER_ONBOARDING'; - -export type RampKnownRole = - | 'AUDITOR' - | 'BUSINESS_ADMIN' - | 'BUSINESS_BOOKKEEPER' - | 'BUSINESS_OWNER' - | 'BUSINESS_USER' - | 'GUEST_USER' - | 'IT_ADMIN'; - -// Ramp can also have custom roles — allow any string -export type RampUserRole = RampKnownRole | (string & {}); - -export interface RampUser { - id: string; - email: string; - first_name?: string; - last_name?: string; - employee_id?: string | null; - status?: RampUserStatus; - role?: RampUserRole; - department_id?: string; - location_id?: string; - manager_id?: string; - phone?: string; - is_manager?: boolean; - business_id?: string; - entity_id?: string; - scheduled_deactivation_date?: string; -} - -export interface RampEmployee { - id: string; - email: string; - name: string; - firstName?: string; - lastName?: string; - employeeId?: string | null; - status: 'active' | 'inactive' | 'suspended' | 'onboarding' | 'invite_pending' | 'invite_expired'; - role?: RampUserRole; - departmentId?: string; - locationId?: string; - managerId?: string; - phone?: string; - isManager?: boolean; -} - -export interface RampPage { - next?: string | null; -} - -export interface RampUsersResponse { - data: RampUser[]; - page: RampPage; -} - -export interface RoleMappingEntry { - rampRole: string; - compRole: string; - isBuiltIn: boolean; - permissions?: Record; - obligations?: Record; -} diff --git a/packages/integration-platform/src/registry/index.ts b/packages/integration-platform/src/registry/index.ts index f4ab59d17f..b2babe6de6 100644 --- a/packages/integration-platform/src/registry/index.ts +++ b/packages/integration-platform/src/registry/index.ts @@ -14,7 +14,6 @@ import { gcpManifest } from '../manifests/gcp'; import { manifest as githubManifest } from '../manifests/github'; import { googleWorkspaceManifest } from '../manifests/google-workspace'; import { manifest as jumpcloudManifest } from '../manifests/jumpcloud'; -import { rampManifest } from '../manifests/ramp'; import { ripplingManifest } from '../manifests/rippling'; import { vercelManifest } from '../manifests/vercel'; @@ -146,7 +145,6 @@ const allManifests: IntegrationManifest[] = [ googleWorkspaceManifest, jumpcloudManifest, ripplingManifest, - rampManifest, vercelManifest, aikidoManifest, ]; From 3d6d1d43fa3820989ee5967313f6ef2c6e25ce5f Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 31 Mar 2026 17:01:32 -0400 Subject: [PATCH 08/31] fix: handle stale Ramp sync provider in legacy orgs - Auto-clear employeeSyncProvider if the manifest no longer exists (covers orgs that had Ramp configured before removal) - Guard Image component against empty src when provider logo is missing Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controllers/sync.controller.ts | 12 ++++++++++++ .../all/components/TeamMembersClient.tsx | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/api/src/integration-platform/controllers/sync.controller.ts b/apps/api/src/integration-platform/controllers/sync.controller.ts index 3c16559545..4d7f3e2a12 100644 --- a/apps/api/src/integration-platform/controllers/sync.controller.ts +++ b/apps/api/src/integration-platform/controllers/sync.controller.ts @@ -1505,6 +1505,18 @@ export class SyncController { throw new HttpException('Organization not found', HttpStatus.NOT_FOUND); } + // Auto-clear stale provider if the manifest no longer exists in the registry + if (org.employeeSyncProvider) { + const manifest = getManifest(org.employeeSyncProvider); + if (!manifest) { + await db.organization.update({ + where: { id: organizationId }, + data: { employeeSyncProvider: null }, + }); + return { provider: null }; + } + } + return { provider: org.employeeSyncProvider, }; diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index 688c4d12a9..7432ccb2c4 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -361,14 +361,16 @@ export function TeamMembersClient({ ) : selectedProvider ? (
- {getProviderName(selectedProvider)} + {getProviderLogo(selectedProvider) && ( + {getProviderName(selectedProvider)} + )} {getProviderName(selectedProvider)}
) : ( From a3313cd6af15193f209d5457f826c753348e3a90 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 31 Mar 2026 17:21:27 -0400 Subject: [PATCH 09/31] fix: scope stale provider cleanup to ramp only Generic manifest check would break dynamic providers during transient registry gaps. Scope to explicit 'ramp' check instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controllers/sync.controller.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/api/src/integration-platform/controllers/sync.controller.ts b/apps/api/src/integration-platform/controllers/sync.controller.ts index 4d7f3e2a12..542ef9c6ad 100644 --- a/apps/api/src/integration-platform/controllers/sync.controller.ts +++ b/apps/api/src/integration-platform/controllers/sync.controller.ts @@ -1505,16 +1505,13 @@ export class SyncController { throw new HttpException('Organization not found', HttpStatus.NOT_FOUND); } - // Auto-clear stale provider if the manifest no longer exists in the registry - if (org.employeeSyncProvider) { - const manifest = getManifest(org.employeeSyncProvider); - if (!manifest) { - await db.organization.update({ - where: { id: organizationId }, - data: { employeeSyncProvider: null }, - }); - return { provider: null }; - } + // Clear stale provider for integrations that have been removed + if (org.employeeSyncProvider === 'ramp') { + await db.organization.update({ + where: { id: organizationId }, + data: { employeeSyncProvider: null }, + }); + return { provider: null }; } return { From 67aacf5b8c038d364eca92021f8dccbab2454d90 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Wed, 1 Apr 2026 11:59:09 -0400 Subject: [PATCH 10/31] fix(portal): remove getJwtToken and use session-cookie auth directly --- .../[orgId]/documents/[formType]/page.tsx | 29 ++----------------- .../documents/[formType]/submissions/page.tsx | 24 ++------------- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx index 746f67eb5e..a9599c5d34 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx @@ -11,28 +11,6 @@ import { PortalFormClient } from './PortalFormClient'; const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024; -async function getJwtToken(cookieHeader: string): Promise { - if (!cookieHeader) return null; - - try { - // Use the main app's auth URL — the API validates JWTs against the main - // app's JWKS, so the token must be issued by the main app, not the portal. - const authUrl = env.APP_AUTH_URL || 'http://localhost:3000'; - const tokenResponse = await fetch(`${authUrl}/api/auth/token`, { - method: 'GET', - headers: { Cookie: cookieHeader }, - }); - - if (!tokenResponse.ok) return null; - - const tokenData = await tokenResponse.json(); - return tokenData?.token ?? null; - } catch (error) { - // Token retrieval failed - caller handles null return - return null; - } -} - export default async function PortalCompanyFormPage({ params, searchParams, @@ -84,17 +62,14 @@ export default async function PortalCompanyFormPage({ const apiUrl = env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'; const cookie = reqHeaders.get('cookie') ?? ''; - - // Get JWT token for API authentication - const jwtToken = await getJwtToken(cookie); - if (!jwtToken) { + if (!cookie) { redirect(`${basePath}?error=${encodeURIComponent('Failed to authenticate with API')}`); } const apiHeaders = { 'Content-Type': 'application/json', 'X-Organization-Id': orgId, - Authorization: `Bearer ${jwtToken}`, + Cookie: cookie, }; try { diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx index 52e4877f51..19887d719f 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx @@ -22,25 +22,6 @@ type SubmissionRow = { } | null; }; -async function getJwtToken(cookieHeader: string): Promise { - if (!cookieHeader) return null; - - try { - const authUrl = env.APP_AUTH_URL || 'http://localhost:3000'; - const tokenResponse = await fetch(`${authUrl}/api/auth/token`, { - method: 'GET', - headers: { Cookie: cookieHeader }, - }); - - if (!tokenResponse.ok) return null; - - const tokenData = await tokenResponse.json(); - return tokenData?.token ?? null; - } catch { - return null; - } -} - export default async function PortalSubmissionsPage({ params, searchParams, @@ -87,11 +68,10 @@ export default async function PortalSubmissionsPage({ const apiUrl = env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'; const cookie = reqHeaders.get('cookie') ?? ''; - const jwtToken = await getJwtToken(cookie); let submissions: SubmissionRow[] = []; - if (jwtToken) { + if (cookie) { try { const res = await fetch( `${apiUrl}/v1/evidence-forms/my-submissions?formType=${formTypeValue}`, @@ -100,7 +80,7 @@ export default async function PortalSubmissionsPage({ headers: { 'Content-Type': 'application/json', 'X-Organization-Id': orgId, - Authorization: `Bearer ${jwtToken}`, + Cookie: cookie, }, cache: 'no-store', }, From ffb260b0220f30bb7f6edd513cb311ee383dcd64 Mon Sep 17 00:00:00 2001 From: Jim Bailey <8464+paradoxbound@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:20:48 +0100 Subject: [PATCH 11/31] fix(auth): make Microsoft OAuth tenantId configurable via env var (#2412) Hardcoded tenantId 'common' allows any Microsoft account to initiate OAuth. Self-hosted operators using Entra ID need to restrict this to their tenant without modifying source (AGPL concern). Read from AUTH_MICROSOFT_TENANT_ID env var, defaulting to 'common'. Also documents the three AUTH_MICROSOFT_* env vars in the self-hosting env reference and API .env.example, where they were previously missing. Fixes #2411 Co-authored-by: Claude Opus 4.6 --- apps/api/.env.example | 5 +++++ apps/api/src/auth/auth.server.ts | 2 +- packages/docs/self-hosting/env-reference.mdx | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 83bf73f587..fb21b77b4f 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -11,6 +11,11 @@ APP_AWS_SECRET_ACCESS_KEY= APP_AWS_ORG_ASSETS_BUCKET= APP_AWS_ENDPOINT="" # optional for using services like MinIO +# Microsoft sign-in (Entra ID / Azure AD) +AUTH_MICROSOFT_CLIENT_ID= +AUTH_MICROSOFT_CLIENT_SECRET= +AUTH_MICROSOFT_TENANT_ID= # 'common' (default), 'organizations', or your tenant GUID + DATABASE_URL= NOVU_API_KEY= diff --git a/apps/api/src/auth/auth.server.ts b/apps/api/src/auth/auth.server.ts index 3832a3f9f3..bfcd3ed1e6 100644 --- a/apps/api/src/auth/auth.server.ts +++ b/apps/api/src/auth/auth.server.ts @@ -179,7 +179,7 @@ if ( socialProviders.microsoft = { clientId: process.env.AUTH_MICROSOFT_CLIENT_ID, clientSecret: process.env.AUTH_MICROSOFT_CLIENT_SECRET, - tenantId: 'common', + tenantId: process.env.AUTH_MICROSOFT_TENANT_ID || 'common', prompt: 'select_account', }; } diff --git a/packages/docs/self-hosting/env-reference.mdx b/packages/docs/self-hosting/env-reference.mdx index 5da63aa08c..3ddf5d6e3c 100644 --- a/packages/docs/self-hosting/env-reference.mdx +++ b/packages/docs/self-hosting/env-reference.mdx @@ -49,6 +49,9 @@ These variables are required for a functional Docker deployment: | `AUTH_GOOGLE_SECRET` | app | runtime | conditional | Google OAuth client secret | | `AUTH_GITHUB_ID` | app | runtime | optional | GitHub OAuth client ID | | `AUTH_GITHUB_SECRET` | app | runtime | optional | GitHub OAuth client secret | +| `AUTH_MICROSOFT_CLIENT_ID` | app | runtime | conditional | Microsoft/Entra OAuth client ID | +| `AUTH_MICROSOFT_CLIENT_SECRET` | app | runtime | conditional | Microsoft/Entra OAuth client secret | +| `AUTH_MICROSOFT_TENANT_ID` | app | runtime | optional | `common` (default), `organizations`, or tenant GUID to restrict login | ### Email From 30516d43f9feccbe1111aeb4838d5e32a4db3ae0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:30:51 -0400 Subject: [PATCH 12/31] feat(app, api, framework-editor): restructure compliance app and add framework editor CLI * feat(app, api, framework-editor): restructure compliance app and add framework editor CLI Restructure the main compliance app to introduce a dedicated /overview landing page and a proper /frameworks table view with drill-down into requirements, controls, and their linked artifacts. App restructure: - Move dashboard (charts, todos, findings) from /frameworks to /overview - Create frameworks table page at /frameworks with compliance %, status - Add control detail as sub-page under /frameworks/:id/controls/:id - Add status and compliance columns to requirements and controls tables - Enhance control satisfaction logic with document freshness (6-month) - Redirect /controls to /frameworks, remove from sidebar - Remove Advanced Mode toggle from settings - Migrate sub-pages from PageWithBreadcrumb to PageLayout + DS Breadcrumb - Migrate tabs to DS Tabs variant="underline" pattern - Move shared types/hooks to @/ paths, eliminate deep relative imports API: - Update frameworks service to include controlDocumentTypes per control - Fetch evidenceSubmissions for document freshness checks - Add framework import/export endpoints and DTOs Framework editor CLI (new package): - Add CLI tool for managing frameworks, controls, requirements, policies, tasks - Device auth flow, session management, config persistence - Commands: framework, control, requirement, policy, task, control-relations - Migration scripts: import-prescient, rebuild-controls, replace-policies, replace-tasks, link-policies-tasks - API client with error handling and formatted output Framework editor app: - Add ImportFrameworkDialog for bulk framework import - Update DataTable, frameworks and requirements pages * fix(FrameworkDeleteDialog, FrameworkRequirements): update button type and add evidenceSubmissions dependency - Changed button type to 'button' in FrameworkDeleteDialog to prevent form submission on cancel. - Added evidenceSubmissions to dependency array in FrameworkRequirements to ensure proper reactivity. * fix: update redirects from frameworks to overview across admin layout and components - Changed redirect paths from '/frameworks' to '/overview' in AdminLayout, ImpersonationBanner, MembersTab, and ControlsPage. - Updated tests to reflect the new redirect behavior. * fix(frameworks-upsert): update content handling in upsertOrgFrameworkStructure - Changed content assignment to ensure it is always an array, using { set: Array.isArray(pt.content) ? pt.content : [pt.content] } for proper Prisma input handling. * fix(frameworks-controls): enhance breadcrumb navigation with requirement links - Updated breadcrumb items to include links to associated requirements when available, improving navigation within the framework control page. * refactor(requirements): remove RequirementControlsTableColumns component and optimize control status retrieval - Deleted the RequirementControlsTableColumns component to streamline the codebase. - Updated getControlStatus function to improve performance by sorting evidence submissions before processing. * chore: remove deprecated scripts and CSV file related to Prescient framework - Deleted prescient.csv and associated import, link, rebuild, replace policies, replace tasks, and test prompt scripts to clean up the codebase and remove unused functionality. - This change streamlines the framework editor CLI by eliminating obsolete components. * chore: remove deprecated scripts and CSV file related to Prescient framework - Deleted prescient.csv and associated import, link, rebuild, replace policies, replace tasks, and test prompt scripts to clean up the codebase and remove unused functionality. - This change streamlines the framework editor CLI by eliminating obsolete components. --------- Co-authored-by: Lewis Carhart --- .../framework/dto/import-framework.dto.ts | 219 +++++++++++ .../framework/framework-export.service.ts | 275 +++++++++++++ .../framework/framework.controller.ts | 14 + .../framework/framework.module.ts | 3 +- .../frameworks/frameworks-upsert.helper.ts | 35 +- apps/api/src/frameworks/frameworks.service.ts | 47 ++- .../admin/components/ImpersonationBanner.tsx | 15 +- .../app/(app)/[orgId]/admin/layout.test.tsx | 6 +- .../src/app/(app)/[orgId]/admin/layout.tsx | 2 +- .../[adminOrgId]/components/MembersTab.tsx | 2 +- .../[orgId]/components/AppShellRailNav.tsx | 4 +- .../[orgId]/components/AppShellWrapper.tsx | 2 +- .../(app)/[orgId]/components/AppSidebar.tsx | 14 +- .../components/app-shell-search-groups.tsx | 28 +- .../[controlId]/components/PoliciesTable.tsx | 1 + .../components/RequirementsTable.tsx | 1 + .../[controlId]/components/SingleControl.tsx | 62 +-- .../[controlId]/components/TasksTable.tsx | 1 + .../src/app/(app)/[orgId]/controls/page.tsx | 74 +--- .../components/FrameworkDeleteDialog.tsx | 32 +- .../components/FrameworkOverview.tsx | 185 ++++----- .../components/FrameworkRequirements.tsx | 138 +++++-- .../controls/[controlId]/loading.tsx | 5 + .../controls/[controlId]/page.tsx | 102 +++++ .../frameworks/[frameworkInstanceId]/page.tsx | 43 ++- .../components/RequirementControls.tsx | 52 ++- .../table/RequirementControlsTable.tsx | 216 ++++++++--- .../table/RequirementControlsTableColumns.tsx | 73 ---- .../requirements/[requirementKey]/page.tsx | 61 ++- .../components/FrameworksPageActions.tsx | 45 +++ .../frameworks/components/FrameworksTable.tsx | 270 +++++++++++++ .../getAllFrameworkInstancesWithControls.ts | 2 +- .../data/getFrameworkWithComplianceScores.ts | 4 +- .../(app)/[orgId]/frameworks/lib/compute.ts | 2 +- .../app/(app)/[orgId]/frameworks/lib/utils.ts | 42 -- .../src/app/(app)/[orgId]/frameworks/page.tsx | 86 ++--- .../components/AddFrameworkModal.test.tsx | 9 +- .../components/AddFrameworkModal.tsx | 2 +- .../components/ComplianceOverview.tsx | 0 .../components/ComplianceProgressChart.tsx | 0 .../components/ConfirmActionDialog.tsx | 0 .../components/FindingsOverview.tsx | 22 +- .../components/FrameworksOverview.test.tsx | 0 .../components/FrameworksOverview.tsx | 92 ++--- .../components/Overview.tsx | 2 +- .../components/PeopleChart.tsx | 0 .../components/PoliciesChart.tsx | 0 .../components/TasksChart.tsx | 0 .../components/ToDoOverview.test.tsx | 6 - .../components/ToDoOverview.tsx | 1 - .../components/types.ts | 11 +- .../src/app/(app)/[orgId]/overview/error.tsx | 24 ++ .../src/app/(app)/[orgId]/overview/layout.tsx | 14 + .../app/(app)/[orgId]/overview/loading.tsx | 5 + .../src/app/(app)/[orgId]/overview/page.tsx | 88 +++++ .../[policyId]/components/PolicyPageTabs.tsx | 40 +- .../src/app/(app)/[orgId]/settings/page.tsx | 5 +- apps/app/src/components/main-menu.tsx | 10 +- .../use-frameworks.ts} | 6 +- apps/app/src/lib/control-compliance.ts | 108 ++++-- apps/app/src/lib/permissions.ts | 5 +- .../types.ts => lib/types/framework.ts} | 10 +- .../frameworks/FrameworksClientPage.tsx | 22 +- .../FrameworkRequirementsClientPage.tsx | 44 ++- .../components/ImportFrameworkDialog.tsx | 233 +++++++++++ .../app/components/DataTable.tsx | 15 +- apps/framework-editor/app/layout.tsx | 2 + apps/framework-editor/prisma/schema.prisma | 6 +- bun.lock | 47 ++- packages/docs/openapi.json | 365 ++++++++++++------ packages/framework-editor-cli/package.json | 30 ++ .../framework-editor-cli/src/commands/auth.ts | 216 +++++++++++ .../src/commands/control-relations.ts | 184 +++++++++ .../src/commands/control.ts | 150 +++++++ .../src/commands/framework.ts | 208 ++++++++++ .../src/commands/policy.ts | 196 ++++++++++ .../src/commands/requirement.ts | 110 ++++++ .../framework-editor-cli/src/commands/task.ts | 174 +++++++++ packages/framework-editor-cli/src/index.ts | 57 +++ .../src/lib/api-client.ts | 111 ++++++ .../framework-editor-cli/src/lib/config.ts | 55 +++ .../framework-editor-cli/src/lib/errors.ts | 43 +++ .../framework-editor-cli/src/lib/output.ts | 100 +++++ packages/framework-editor-cli/src/types.ts | 115 ++++++ packages/framework-editor-cli/tsconfig.json | 9 + packages/framework-editor-cli/tsup.config.ts | 12 + .../editor/utils/validate-content.ts | 58 ++- 87 files changed, 4250 insertions(+), 935 deletions(-) create mode 100644 apps/api/src/framework-editor/framework/dto/import-framework.dto.ts create mode 100644 apps/api/src/framework-editor/framework/framework-export.service.ts create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/loading.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/page.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksPageActions.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksTable.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/lib/utils.ts rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/AddFrameworkModal.test.tsx (94%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/AddFrameworkModal.tsx (98%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/ComplianceOverview.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/ComplianceProgressChart.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/ConfirmActionDialog.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/FindingsOverview.tsx (89%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/FrameworksOverview.test.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/FrameworksOverview.tsx (63%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/Overview.tsx (98%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/PeopleChart.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/PoliciesChart.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/TasksChart.tsx (100%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/ToDoOverview.test.tsx (97%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/ToDoOverview.tsx (99%) rename apps/app/src/app/(app)/[orgId]/{frameworks => overview}/components/types.ts (71%) create mode 100644 apps/app/src/app/(app)/[orgId]/overview/error.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/overview/layout.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/overview/loading.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/overview/page.tsx rename apps/app/src/{app/(app)/[orgId]/frameworks/hooks/useFrameworks.ts => hooks/use-frameworks.ts} (94%) rename apps/app/src/{app/(app)/[orgId]/frameworks/types.ts => lib/types/framework.ts} (63%) create mode 100644 apps/framework-editor/app/(pages)/frameworks/components/ImportFrameworkDialog.tsx create mode 100644 packages/framework-editor-cli/package.json create mode 100644 packages/framework-editor-cli/src/commands/auth.ts create mode 100644 packages/framework-editor-cli/src/commands/control-relations.ts create mode 100644 packages/framework-editor-cli/src/commands/control.ts create mode 100644 packages/framework-editor-cli/src/commands/framework.ts create mode 100644 packages/framework-editor-cli/src/commands/policy.ts create mode 100644 packages/framework-editor-cli/src/commands/requirement.ts create mode 100644 packages/framework-editor-cli/src/commands/task.ts create mode 100644 packages/framework-editor-cli/src/index.ts create mode 100644 packages/framework-editor-cli/src/lib/api-client.ts create mode 100644 packages/framework-editor-cli/src/lib/config.ts create mode 100644 packages/framework-editor-cli/src/lib/errors.ts create mode 100644 packages/framework-editor-cli/src/lib/output.ts create mode 100644 packages/framework-editor-cli/src/types.ts create mode 100644 packages/framework-editor-cli/tsconfig.json create mode 100644 packages/framework-editor-cli/tsup.config.ts diff --git a/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts b/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts new file mode 100644 index 0000000000..e0d698681a --- /dev/null +++ b/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts @@ -0,0 +1,219 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsString, + IsNotEmpty, + IsBoolean, + IsOptional, + IsArray, + IsInt, + IsObject, + IsEnum, + MaxLength, + ValidateNested, + ArrayMaxSize, + Min, +} from 'class-validator'; +import { + EvidenceFormType, + Frequency, + Departments, + TaskAutomationStatus, +} from '@trycompai/db'; +import { MaxJsonSize } from '../../validators/max-json-size.validator'; + +class ImportFrameworkMetaDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(255) + name: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(50) + version: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(2000) + description: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + visible?: boolean; +} + +class ImportRequirementDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(255) + name: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @MaxLength(255) + identifier?: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(5000) + description: string; +} + +class ImportControlTemplateDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(255) + name: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(5000) + description: string; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(50) + @IsEnum(EvidenceFormType, { each: true }) + @IsOptional() + documentTypes?: EvidenceFormType[]; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(500) + @IsInt({ each: true }) + @Min(0, { each: true }) + @IsOptional() + requirementIndices?: number[]; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(500) + @IsInt({ each: true }) + @Min(0, { each: true }) + @IsOptional() + policyTemplateIndices?: number[]; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(500) + @IsInt({ each: true }) + @Min(0, { each: true }) + @IsOptional() + taskTemplateIndices?: number[]; +} + +class ImportPolicyTemplateDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(255) + name: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(5000) + description: string; + + @ApiProperty() + @IsEnum(Frequency) + frequency: Frequency; + + @ApiProperty() + @IsEnum(Departments) + department: Departments; + + @ApiPropertyOptional() + @IsObject() + @IsOptional() + @MaxJsonSize() + content?: Record; +} + +class ImportTaskTemplateDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(255) + name: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + @MaxLength(5000) + description: string; + + @ApiProperty() + @IsEnum(Frequency) + frequency: Frequency; + + @ApiProperty() + @IsEnum(Departments) + department: Departments; + + @ApiPropertyOptional() + @IsEnum(TaskAutomationStatus) + @IsOptional() + automationStatus?: TaskAutomationStatus; +} + +export class ImportFrameworkDto { + @ApiProperty({ example: '1' }) + @IsString() + @IsNotEmpty() + @MaxLength(10) + version: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @MaxLength(50) + exportedAt?: string; + + @ApiProperty() + @ValidateNested() + @Type(() => ImportFrameworkMetaDto) + framework: ImportFrameworkMetaDto; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(1000) + @ValidateNested({ each: true }) + @Type(() => ImportRequirementDto) + @IsOptional() + requirements?: ImportRequirementDto[]; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(1000) + @ValidateNested({ each: true }) + @Type(() => ImportControlTemplateDto) + @IsOptional() + controlTemplates?: ImportControlTemplateDto[]; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(500) + @ValidateNested({ each: true }) + @Type(() => ImportPolicyTemplateDto) + @IsOptional() + policyTemplates?: ImportPolicyTemplateDto[]; + + @ApiPropertyOptional() + @IsArray() + @ArrayMaxSize(500) + @ValidateNested({ each: true }) + @Type(() => ImportTaskTemplateDto) + @IsOptional() + taskTemplates?: ImportTaskTemplateDto[]; +} diff --git a/apps/api/src/framework-editor/framework/framework-export.service.ts b/apps/api/src/framework-editor/framework/framework-export.service.ts new file mode 100644 index 0000000000..a2273d3f46 --- /dev/null +++ b/apps/api/src/framework-editor/framework/framework-export.service.ts @@ -0,0 +1,275 @@ +import { + Injectable, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { db, Prisma, EvidenceFormType } from '@trycompai/db'; +import type { ImportFrameworkDto } from './dto/import-framework.dto'; + +export interface ExportedFramework { + version: string; + exportedAt: string; + framework: { + name: string; + version: string; + description: string; + visible: boolean; + }; + requirements: Array<{ + name: string; + identifier: string; + description: string; + }>; + controlTemplates: Array<{ + name: string; + description: string; + documentTypes: string[]; + requirementIndices: number[]; + policyTemplateIndices: number[]; + taskTemplateIndices: number[]; + }>; + policyTemplates: Array<{ + name: string; + description: string; + frequency: string; + department: string; + content: Record; + }>; + taskTemplates: Array<{ + name: string; + description: string; + frequency: string; + department: string; + automationStatus: string; + }>; +} + +@Injectable() +export class FrameworkExportService { + private readonly logger = new Logger(FrameworkExportService.name); + + async export(frameworkId: string): Promise { + const framework = await db.frameworkEditorFramework.findUnique({ + where: { id: frameworkId }, + }); + + if (!framework) { + throw new NotFoundException(`Framework ${frameworkId} not found`); + } + + const requirements = await db.frameworkEditorRequirement.findMany({ + where: { frameworkId }, + orderBy: { name: 'asc' }, + }); + + const controlTemplates = await db.frameworkEditorControlTemplate.findMany({ + where: { requirements: { some: { frameworkId } } }, + include: { + requirements: { select: { id: true }, where: { frameworkId } }, + policyTemplates: { select: { id: true } }, + taskTemplates: { select: { id: true } }, + }, + orderBy: { name: 'asc' }, + }); + + const policyIds = new Set( + controlTemplates.flatMap((ct) => ct.policyTemplates.map((p) => p.id)), + ); + const taskIds = new Set( + controlTemplates.flatMap((ct) => ct.taskTemplates.map((t) => t.id)), + ); + + const policyTemplates = await db.frameworkEditorPolicyTemplate.findMany({ + where: { id: { in: [...policyIds] } }, + orderBy: { name: 'asc' }, + }); + + const taskTemplates = await db.frameworkEditorTaskTemplate.findMany({ + where: { id: { in: [...taskIds] } }, + orderBy: { name: 'asc' }, + }); + + const reqIdToIndex = new Map(requirements.map((r, i) => [r.id, i])); + const policyIdToIndex = new Map(policyTemplates.map((p, i) => [p.id, i])); + const taskIdToIndex = new Map(taskTemplates.map((t, i) => [t.id, i])); + + this.logger.log( + `Exporting framework "${framework.name}": ${requirements.length} requirements, ` + + `${controlTemplates.length} controls, ${policyTemplates.length} policies, ` + + `${taskTemplates.length} tasks`, + ); + + return { + version: '1', + exportedAt: new Date().toISOString(), + framework: { + name: framework.name, + version: framework.version, + description: framework.description, + visible: framework.visible, + }, + requirements: requirements.map((r) => ({ + name: r.name, + identifier: r.identifier, + description: r.description, + })), + controlTemplates: controlTemplates.map((ct) => ({ + name: ct.name, + description: ct.description, + documentTypes: ct.documentTypes as string[], + requirementIndices: ct.requirements + .map((r) => reqIdToIndex.get(r.id)) + .filter((i): i is number => i !== undefined), + policyTemplateIndices: ct.policyTemplates + .map((p) => policyIdToIndex.get(p.id)) + .filter((i): i is number => i !== undefined), + taskTemplateIndices: ct.taskTemplates + .map((t) => taskIdToIndex.get(t.id)) + .filter((i): i is number => i !== undefined), + })), + policyTemplates: policyTemplates.map((p) => ({ + name: p.name, + description: p.description, + frequency: p.frequency, + department: p.department, + content: p.content as Record, + })), + taskTemplates: taskTemplates.map((t) => ({ + name: t.name, + description: t.description, + frequency: t.frequency, + department: t.department, + automationStatus: t.automationStatus, + })), + }; + } + + async import(dto: ImportFrameworkDto) { + if (dto.version !== '1') { + throw new BadRequestException( + `Unsupported export version "${dto.version}". Expected "1".`, + ); + } + + this.validateIndices(dto); + + return db.$transaction(async (tx) => { + const framework = await tx.frameworkEditorFramework.create({ + data: { + name: dto.framework.name, + version: dto.framework.version, + description: dto.framework.description, + visible: dto.framework.visible ?? false, + }, + }); + + const createdRequirements = await Promise.all( + (dto.requirements ?? []).map((r) => + tx.frameworkEditorRequirement.create({ + data: { + frameworkId: framework.id, + name: r.name, + identifier: r.identifier ?? '', + description: r.description, + }, + }), + ), + ); + + const createdPolicies = await Promise.all( + (dto.policyTemplates ?? []).map((p) => + tx.frameworkEditorPolicyTemplate.create({ + data: { + name: p.name, + description: p.description, + frequency: p.frequency, + department: p.department, + content: (p.content ?? {}) as Prisma.InputJsonValue, + }, + }), + ), + ); + + const createdTasks = await Promise.all( + (dto.taskTemplates ?? []).map((t) => + tx.frameworkEditorTaskTemplate.create({ + data: { + name: t.name, + description: t.description, + frequency: t.frequency, + department: t.department, + automationStatus: t.automationStatus, + }, + }), + ), + ); + + await Promise.all( + (dto.controlTemplates ?? []).map((ct) => + tx.frameworkEditorControlTemplate.create({ + data: { + name: ct.name, + description: ct.description, + documentTypes: (ct.documentTypes ?? []) as EvidenceFormType[], + requirements: { + connect: (ct.requirementIndices ?? []).map((i) => ({ + id: createdRequirements[i].id, + })), + }, + policyTemplates: { + connect: (ct.policyTemplateIndices ?? []).map((i) => ({ + id: createdPolicies[i].id, + })), + }, + taskTemplates: { + connect: (ct.taskTemplateIndices ?? []).map((i) => ({ + id: createdTasks[i].id, + })), + }, + }, + }), + ), + ); + + this.logger.log( + `Imported framework "${framework.name}" (${framework.id}): ` + + `${createdRequirements.length} requirements, ` + + `${dto.controlTemplates?.length ?? 0} controls, ` + + `${createdPolicies.length} policies, ${createdTasks.length} tasks`, + ); + + return framework; + }); + } + + private validateIndices(dto: ImportFrameworkDto) { + const reqCount = dto.requirements?.length ?? 0; + const policyCount = dto.policyTemplates?.length ?? 0; + const taskCount = dto.taskTemplates?.length ?? 0; + + for (const ct of dto.controlTemplates ?? []) { + for (const i of ct.requirementIndices ?? []) { + if (i < 0 || i >= reqCount) { + throw new BadRequestException( + `Control "${ct.name}" references requirement index ${i}, but only ${reqCount} requirements exist`, + ); + } + } + for (const i of ct.policyTemplateIndices ?? []) { + if (i < 0 || i >= policyCount) { + throw new BadRequestException( + `Control "${ct.name}" references policy template index ${i}, but only ${policyCount} policy templates exist`, + ); + } + } + for (const i of ct.taskTemplateIndices ?? []) { + if (i < 0 || i >= taskCount) { + throw new BadRequestException( + `Control "${ct.name}" references task template index ${i}, but only ${taskCount} task templates exist`, + ); + } + } + } + } +} diff --git a/apps/api/src/framework-editor/framework/framework.controller.ts b/apps/api/src/framework-editor/framework/framework.controller.ts index 0805a5b040..4564b32df0 100644 --- a/apps/api/src/framework-editor/framework/framework.controller.ts +++ b/apps/api/src/framework-editor/framework/framework.controller.ts @@ -14,7 +14,9 @@ import { import { ApiTags } from '@nestjs/swagger'; import { PlatformAdminGuard } from '../../auth/platform-admin.guard'; import { CreateFrameworkDto } from './dto/create-framework.dto'; +import { ImportFrameworkDto } from './dto/import-framework.dto'; import { UpdateFrameworkDto } from './dto/update-framework.dto'; +import { FrameworkExportService } from './framework-export.service'; import { FrameworkEditorFrameworkService } from './framework.service'; @ApiTags('Framework Editor Frameworks') @@ -23,6 +25,7 @@ import { FrameworkEditorFrameworkService } from './framework.service'; export class FrameworkEditorFrameworkController { constructor( private readonly frameworkService: FrameworkEditorFrameworkService, + private readonly exportService: FrameworkExportService, ) {} @Get() @@ -46,6 +49,17 @@ export class FrameworkEditorFrameworkController { return this.frameworkService.create(dto); } + @Post('import') + @UsePipes(new ValidationPipe({ whitelist: true, transform: true })) + async importFramework(@Body() dto: ImportFrameworkDto) { + return this.exportService.import(dto); + } + + @Get(':id/export') + async exportFramework(@Param('id') id: string) { + return this.exportService.export(id); + } + @Patch(':id') @UsePipes(new ValidationPipe({ whitelist: true, transform: true })) async update(@Param('id') id: string, @Body() dto: UpdateFrameworkDto) { diff --git a/apps/api/src/framework-editor/framework/framework.module.ts b/apps/api/src/framework-editor/framework/framework.module.ts index 45b39638a1..bf0b5b3972 100644 --- a/apps/api/src/framework-editor/framework/framework.module.ts +++ b/apps/api/src/framework-editor/framework/framework.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '../../auth/auth.module'; +import { FrameworkExportService } from './framework-export.service'; import { FrameworkEditorFrameworkController } from './framework.controller'; import { FrameworkEditorFrameworkService } from './framework.service'; @Module({ imports: [AuthModule], controllers: [FrameworkEditorFrameworkController], - providers: [FrameworkEditorFrameworkService], + providers: [FrameworkEditorFrameworkService, FrameworkExportService], exports: [FrameworkEditorFrameworkService], }) export class FrameworkEditorFrameworkModule {} diff --git a/apps/api/src/frameworks/frameworks-upsert.helper.ts b/apps/api/src/frameworks/frameworks-upsert.helper.ts index e1c86049bf..1f47846a96 100644 --- a/apps/api/src/frameworks/frameworks-upsert.helper.ts +++ b/apps/api/src/frameworks/frameworks-upsert.helper.ts @@ -1,5 +1,33 @@ import { Prisma } from '@trycompai/db'; +/** + * Unwraps a `{ set: [...] }` wrapper that was incorrectly stored by a + * previous createMany bug for Json[] fields. Returns the inner array if + * the pattern is detected, otherwise returns the original value. + * Filters null entries and returns InputJsonValue[] for createMany compatibility. + */ +function sanitizeJsonContent( + value: Prisma.JsonValue[], +): Prisma.InputJsonValue[] { + let arr = value; + + if ( + arr.length === 1 && + arr[0] && + typeof arr[0] === 'object' && + !Array.isArray(arr[0]) && + 'set' in arr[0] && + Array.isArray(arr[0].set) + ) { + arr = arr[0].set as Prisma.JsonValue[]; + } + + // JsonValue and InputJsonValue are runtime-identical; only difference + // is the null union member which we filter out here. + const filtered: unknown[] = arr.filter((v) => v != null); + return filtered as Prisma.InputJsonValue[]; +} + type FrameworkEditorFrameworkWithRequirements = Prisma.FrameworkEditorFrameworkGetPayload<{ include: { requirements: true }; @@ -152,8 +180,9 @@ export async function upsertOrgFrameworkStructure({ description: pt.description, department: pt.department, frequency: pt.frequency, - content: - pt.content as Prisma.PolicyCreateInput['content'], + content: sanitizeJsonContent( + Array.isArray(pt.content) ? pt.content : [pt.content], + ), organizationId, policyTemplateId: pt.id, })), @@ -172,7 +201,7 @@ export async function upsertOrgFrameworkStructure({ data: newPolicies.map((p) => ({ policyId: p.id, version: 1, - content: p.content as Prisma.InputJsonValue[], + content: sanitizeJsonContent(p.content), changelog: 'Initial version from template', })), }); diff --git a/apps/api/src/frameworks/frameworks.service.ts b/apps/api/src/frameworks/frameworks.service.ts index 4b69e33ec9..a00f341af7 100644 --- a/apps/api/src/frameworks/frameworks.service.ts +++ b/apps/api/src/frameworks/frameworks.service.ts @@ -3,7 +3,7 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db, type EvidenceFormType } from '@trycompai/db'; import { getOverviewScores, getCurrentMember, @@ -94,6 +94,7 @@ export class FrameworksService { select: { id: true, name: true, status: true }, }, requirementsMapped: true, + controlDocumentTypes: true, }, }, }, @@ -114,13 +115,21 @@ export class FrameworksService { ...controlData, policies: rm.control.policies || [], requirementsMapped: rm.control.requirementsMapped || [], + controlDocumentTypes: rm.control.controlDocumentTypes || [], }); } } const { requirementsMapped: _, ...rest } = fi; - // Fetch additional data - const [requirementDefinitions, tasks, requirementMaps] = + // Collect all required evidence form types across all controls + const allFormTypes = new Set(); + for (const control of controlsMap.values()) { + for (const dt of control.controlDocumentTypes) { + allFormTypes.add(dt.formType); + } + } + + const [requirementDefinitions, tasks, requirementMaps, evidenceSubmissions] = await Promise.all([ db.frameworkEditorRequirement.findMany({ where: { frameworkId: fi.frameworkId }, @@ -134,6 +143,16 @@ export class FrameworksService { where: { frameworkInstanceId }, include: { control: true }, }), + allFormTypes.size > 0 + ? db.evidenceSubmission.findMany({ + where: { + organizationId, + formType: { in: Array.from(allFormTypes) }, + }, + select: { id: true, formType: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }) + : Promise.resolve([]), ]); return { @@ -142,6 +161,7 @@ export class FrameworksService { requirementDefinitions, tasks, requirementMaps, + evidenceSubmissions, }; } @@ -218,6 +238,7 @@ export class FrameworksService { policies: { select: { id: true, name: true, status: true }, }, + controlDocumentTypes: true, }, }, }, @@ -233,6 +254,25 @@ export class FrameworksService { throw new NotFoundException('Requirement not found'); } + // Collect evidence form types for related controls + const formTypes = new Set(); + for (const rc of relatedControls) { + for (const dt of rc.control.controlDocumentTypes || []) { + formTypes.add(dt.formType); + } + } + + const evidenceSubmissions = formTypes.size > 0 + ? await db.evidenceSubmission.findMany({ + where: { + organizationId, + formType: { in: Array.from(formTypes) }, + }, + select: { id: true, formType: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }) + : []; + const siblingRequirements = allReqDefs .filter((r) => r.id !== requirementKey) .map((r) => ({ id: r.id, name: r.name })); @@ -241,6 +281,7 @@ export class FrameworksService { requirement, relatedControls, tasks, + evidenceSubmissions, siblingRequirements, }; } diff --git a/apps/app/src/app/(app)/[orgId]/admin/components/ImpersonationBanner.tsx b/apps/app/src/app/(app)/[orgId]/admin/components/ImpersonationBanner.tsx index 5b52154a95..cfb6d47b21 100644 --- a/apps/app/src/app/(app)/[orgId]/admin/components/ImpersonationBanner.tsx +++ b/apps/app/src/app/(app)/[orgId]/admin/components/ImpersonationBanner.tsx @@ -1,13 +1,12 @@ 'use client'; import { authClient, useSession } from '@/utils/auth-client'; -import { usePathname, useRouter } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import { useState } from 'react'; export function ImpersonationBanner() { const { data: session } = useSession(); const router = useRouter(); - const pathname = usePathname(); const [stopping, setStopping] = useState(false); const rawImpersonatedBy = ( @@ -18,16 +17,22 @@ export function ImpersonationBanner() { if (!impersonatedBy) return null; - const orgId = pathname?.split('/')[1] ?? ''; - const handleStop = async () => { setStopping(true); try { await authClient.admin.stopImpersonating(); + const { data: restored } = await authClient.getSession(); (authClient.$store as { notify: (signal: string) => void }).notify( '$sessionSignal', ); - router.push(`/${orgId}/admin/organizations`); + const adminOrgId = ( + restored?.session as Record | undefined + )?.activeOrganizationId; + if (typeof adminOrgId === 'string' && adminOrgId) { + router.push(`/${adminOrgId}/admin/organizations`); + } else { + router.push('/'); + } router.refresh(); } catch { setStopping(false); diff --git a/apps/app/src/app/(app)/[orgId]/admin/layout.test.tsx b/apps/app/src/app/(app)/[orgId]/admin/layout.test.tsx index 7278464481..d47a7c7770 100644 --- a/apps/app/src/app/(app)/[orgId]/admin/layout.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/admin/layout.test.tsx @@ -37,7 +37,7 @@ describe('[orgId]/admin/layout - auth gate', () => { }), ).rejects.toThrow('NEXT_REDIRECT'); - expect(mockRedirect).toHaveBeenCalledWith('/org_1/frameworks'); + expect(mockRedirect).toHaveBeenCalledWith('/org_1/overview'); }); it('redirects to frameworks when user role is not admin', async () => { @@ -53,7 +53,7 @@ describe('[orgId]/admin/layout - auth gate', () => { }), ).rejects.toThrow('NEXT_REDIRECT'); - expect(mockRedirect).toHaveBeenCalledWith('/org_1/frameworks'); + expect(mockRedirect).toHaveBeenCalledWith('/org_1/overview'); }); it('redirects to frameworks when user role is null', async () => { @@ -69,7 +69,7 @@ describe('[orgId]/admin/layout - auth gate', () => { }), ).rejects.toThrow('NEXT_REDIRECT'); - expect(mockRedirect).toHaveBeenCalledWith('/org_1/frameworks'); + expect(mockRedirect).toHaveBeenCalledWith('/org_1/overview'); }); it('renders children when user is a platform admin', async () => { diff --git a/apps/app/src/app/(app)/[orgId]/admin/layout.tsx b/apps/app/src/app/(app)/[orgId]/admin/layout.tsx index 6ba1e21520..d0af2d59fb 100644 --- a/apps/app/src/app/(app)/[orgId]/admin/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/admin/layout.tsx @@ -15,7 +15,7 @@ export default async function AdminLayout({ }); if (!session?.user?.id || session.user.role !== 'admin') { - redirect(`/${orgId}/frameworks`); + redirect(`/${orgId}/overview`); } return <>{children}; diff --git a/apps/app/src/app/(app)/[orgId]/admin/organizations/[adminOrgId]/components/MembersTab.tsx b/apps/app/src/app/(app)/[orgId]/admin/organizations/[adminOrgId]/components/MembersTab.tsx index 9da871326c..a066232f39 100644 --- a/apps/app/src/app/(app)/[orgId]/admin/organizations/[adminOrgId]/components/MembersTab.tsx +++ b/apps/app/src/app/(app)/[orgId]/admin/organizations/[adminOrgId]/components/MembersTab.tsx @@ -139,7 +139,7 @@ export function MembersTab({ try { await authClient.admin.impersonateUser({ userId }); await authClient.organization.setActive({ organizationId: orgId }); - router.push(`/${orgId}/frameworks`); + router.push(`/${orgId}/overview`); } catch (err) { console.error('Impersonation failed:', err); setImpersonatingUserId(null); diff --git a/apps/app/src/app/(app)/[orgId]/components/AppShellRailNav.tsx b/apps/app/src/app/(app)/[orgId]/components/AppShellRailNav.tsx index 306a908479..5495791d66 100644 --- a/apps/app/src/app/(app)/[orgId]/components/AppShellRailNav.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/AppShellRailNav.tsx @@ -29,10 +29,10 @@ export function AppShellRailNav({ organizationId }: AppShellRailNavProps) { const items = [ { - href: `${orgBase}/frameworks`, + href: `${orgBase}/overview`, label: 'Overview', icon: , - isActive: isActivePrefix(`${orgBase}/frameworks`), + isActive: isActivePrefix(`${orgBase}/overview`), }, { href: `${orgBase}/policies`, diff --git a/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx b/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx index 8e83902972..c336b4bc88 100644 --- a/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx @@ -250,7 +250,7 @@ function AppShellWrapperContent({ {canAccessCompliance(permissions) && ( } label="Compliance" diff --git a/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx b/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx index a3198136a4..830ca29e41 100644 --- a/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx @@ -31,10 +31,16 @@ export function AppSidebar({ const pathname = usePathname() ?? ''; const navItems: NavItem[] = [ + { + id: 'overview', + path: `/${organization.id}/overview`, + name: 'Overview', + hidden: !canAccessRoute(permissions, 'overview'), + }, { id: 'frameworks', path: `/${organization.id}/frameworks`, - name: 'Overview', + name: 'Frameworks', hidden: !canAccessRoute(permissions, 'frameworks'), }, { @@ -43,12 +49,6 @@ export function AppSidebar({ name: 'Auditor View', hidden: !hasAuditorRole || !canAccessRoute(permissions, 'auditor'), }, - { - id: 'controls', - path: `/${organization.id}/controls`, - name: 'Controls', - hidden: !organization.advancedModeEnabled || !canAccessRoute(permissions, 'controls'), - }, { id: 'policies', path: `/${organization.id}/policies`, diff --git a/apps/app/src/app/(app)/[orgId]/components/app-shell-search-groups.tsx b/apps/app/src/app/(app)/[orgId]/components/app-shell-search-groups.tsx index 21938cae19..11e4359961 100644 --- a/apps/app/src/app/(app)/[orgId]/components/app-shell-search-groups.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/app-shell-search-groups.tsx @@ -72,14 +72,26 @@ export const getAppShellSearchGroups = ({ const can = (route: string) => canAccessRoute(permissions, route); const baseItems = [ - ...(can('frameworks') + ...(can('overview') ? [ createNavItem({ id: 'overview', label: 'Overview', icon: , + path: `/${organizationId}/overview`, + keywords: ['dashboard', 'home', 'overview'], + router, + }), + ] + : []), + ...(can('frameworks') + ? [ + createNavItem({ + id: 'frameworks', + label: 'Frameworks', + icon: , path: `/${organizationId}/frameworks`, - keywords: ['dashboard', 'home', 'frameworks'], + keywords: ['frameworks', 'compliance', 'standards'], router, }), ] @@ -96,18 +108,6 @@ export const getAppShellSearchGroups = ({ }), ] : []), - ...(isAdvancedModeEnabled && can('controls') - ? [ - createNavItem({ - id: 'controls', - label: 'Controls', - icon: , - path: `/${organizationId}/controls`, - keywords: ['security', 'compliance'], - router, - }), - ] - : []), ...(can('policies') ? [ createNavItem({ diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx index 37a3a5c521..1a4a0212d7 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx @@ -78,6 +78,7 @@ export function PoliciesTable({ policies, orgId }: PoliciesTableProps) { key={policy.id} role="button" tabIndex={0} + style={{ cursor: 'pointer' }} onClick={() => handleRowClick(policy.id)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx index f0bee7cbdd..10ca7f8d0c 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx @@ -93,6 +93,7 @@ export function RequirementsTable({ requirements, orgId }: RequirementsTableProp key={requirement.id} role="button" tabIndex={0} + style={{ cursor: 'pointer' }} onClick={() => handleRowClick(requirement)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx index e7898169d1..1ef77755a8 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx @@ -1,7 +1,5 @@ 'use client'; -import { StatusIndicator } from '@/components/status-indicator'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@trycompai/ui/tabs'; import type { Control, FrameworkEditorFramework, @@ -11,8 +9,14 @@ import type { RequirementMap, Task, } from '@db'; +import { + Stack, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@trycompai/design-system'; import { useParams } from 'next/navigation'; -import { useMemo } from 'react'; import type { ControlProgressResponse } from '../data/getOrganizationControlProgress'; import { PoliciesTable } from './PoliciesTable'; import { RequirementsTable } from './RequirementsTable'; @@ -41,62 +45,32 @@ export function SingleControl({ }: SingleControlProps) { const params = useParams<{ orgId: string; controlId: string }>(); const orgIdFromParams = params.orgId; - const controlIdFromParams = params.controlId; - - const progressStatus = useMemo(() => { - if (!controlProgress) return 'not_started'; - if (controlProgress.total === controlProgress.completed) return 'completed'; - if (controlProgress.completed > 0) return 'in_progress'; - - // Check if any task is not "todo" or any policy is not "draft" - const anyTaskInProgress = relatedTasks.some((task) => task.status !== 'todo'); - const anyPolicyInProgress = relatedPolicies.some((policy) => policy.status !== 'draft'); - if (anyTaskInProgress || anyPolicyInProgress) return 'in_progress'; - - return 'not_started'; - }, [controlProgress, relatedPolicies, relatedTasks]); if (!control || !controlProgress) { return ; } return ( -
- {/* Tabbed Content */} - - - - Policies - - {relatedPolicies.length} - - - - Tasks - - {relatedTasks.length} - - - - Requirements - - {control.requirementsMapped.length} - - + + + + Policies ({relatedPolicies.length}) + Tasks ({relatedTasks.length}) + Requirements ({control.requirementsMapped.length}) - + - + - + - -
+ + ); } diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx index 14e019108a..99f1c92727 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx @@ -80,6 +80,7 @@ export function TasksTable({ tasks, orgId }: TasksTableProps) { key={task.id} role="button" tabIndex={0} + style={{ cursor: 'pointer' }} onClick={() => handleRowClick(task.id)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { diff --git a/apps/app/src/app/(app)/[orgId]/controls/page.tsx b/apps/app/src/app/(app)/[orgId]/controls/page.tsx index 61e6ca895d..526ad56fdd 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/page.tsx @@ -1,74 +1,10 @@ -import { serverApi } from '@/lib/api-server'; -import { PageHeader, PageLayout, Stack } from '@trycompai/design-system'; -import { Metadata } from 'next'; -import { SearchParams } from 'nuqs'; -import { CreateControlSheet } from './components/CreateControlSheet'; -import { ControlsTable } from './components/controls-table'; -import type { ControlWithRelations } from './data/queries'; -import { searchParamsCache } from './data/validations'; +import { redirect } from 'next/navigation'; -interface ControlTableProps { - searchParams: Promise; +interface ControlsPageProps { params: Promise<{ orgId: string }>; } -export async function generateMetadata(): Promise { - return { - title: 'Controls', - }; -} - -export default async function ControlsPage({ ...props }: ControlTableProps) { - const searchParams = await props.searchParams; - const search = searchParamsCache.parse(searchParams); - const sort = search.sort?.[0]; - - const queryParams = new URLSearchParams({ - page: String(search.page), - perPage: String(search.perPage), - ...(search.name && { name: search.name }), - ...(sort && { sortBy: sort.id, sortDesc: String(sort.desc) }), - }); - - const [controlsRes, optionsRes] = await Promise.all([ - serverApi.get<{ data: ControlWithRelations[]; pageCount: number }>( - `/v1/controls?${queryParams}`, - ), - serverApi.get<{ - policies: { id: string; name: string }[]; - tasks: { id: string; title: string }[]; - requirements: { - id: string; - name: string; - identifier: string; - frameworkInstanceId: string; - frameworkName: string; - }[]; - }>('/v1/controls/options'), - ]); - - const controlsData = controlsRes.data ?? { data: [], pageCount: 0 }; - const options = optionsRes.data ?? { policies: [], tasks: [], requirements: [] }; - - const promises = Promise.resolve( - [controlsData] as [{ data: ControlWithRelations[]; pageCount: number }], - ); - - return ( - - - - } - /> - - - - ); +export default async function ControlsPage({ params }: ControlsPageProps) { + const { orgId } = await params; + redirect(`/${orgId}/overview`); } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx index ecc0175ae5..eb4c3f57d5 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx @@ -1,6 +1,7 @@ 'use client'; -import { Button } from '@trycompai/ui/button'; +import { Button } from '@trycompai/design-system'; +import { TrashCan } from '@trycompai/design-system/icons'; import { Dialog, DialogContent, @@ -12,14 +13,13 @@ import { import { Form } from '@trycompai/ui/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { usePermissions } from '@/hooks/use-permissions'; -import { Trash2 } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; -import { FrameworkInstanceWithControls } from '../../types'; -import { useFrameworks } from '../../hooks/useFrameworks'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; +import { useFrameworks } from '@/hooks/use-frameworks'; const formSchema = z.object({ comment: z.string().optional(), @@ -55,9 +55,9 @@ export function FrameworkDeleteDialog({ setIsSubmitting(true); try { await deleteFramework(frameworkInstance.id); - toast.info('Framework deleted! Redirecting to frameworks list...'); + toast.info('Framework deleted! Redirecting to overview...'); onClose(); - router.push(`/${frameworkInstance.organizationId}/frameworks`); + router.push(`/${frameworkInstance.organizationId}/overview`); } catch { toast.error('Failed to delete framework.'); setIsSubmitting(false); @@ -79,18 +79,14 @@ export function FrameworkDeleteDialog({ - diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx index 95681ed89f..f458b56b91 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx @@ -1,57 +1,63 @@ 'use client'; -import { Badge } from '@trycompai/ui/badge'; -import { Button } from '@trycompai/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; -import { cn } from '@trycompai/ui/cn'; +import type { Control, Task } from '@db'; +import { + Badge, + Button, + PageHeader, + Text, +} from '@trycompai/design-system'; +import { TrashCan, OverflowMenuVertical } from '@trycompai/design-system/icons'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@trycompai/ui/dropdown-menu'; -import { Progress } from '@trycompai/ui/progress'; -import { Control, Task } from '@db'; -import { BarChart3, MoreVertical, Target, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { usePermissions } from '@/hooks/use-permissions'; -import { getControlStatus } from '../../lib/utils'; -import { FrameworkInstanceWithControls } from '../../types'; +import { getControlStatus } from '@/lib/control-compliance'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; import { FrameworkDeleteDialog } from './FrameworkDeleteDialog'; +interface EvidenceSubmissionInfo { + id: string; + formType: string; + createdAt: Date | string; +} + interface FrameworkOverviewProps { frameworkInstanceWithControls: FrameworkInstanceWithControls; tasks: (Task & { controls: Control[] })[]; + evidenceSubmissions?: EvidenceSubmissionInfo[]; } export function FrameworkOverview({ frameworkInstanceWithControls, tasks, + evidenceSubmissions = [], }: FrameworkOverviewProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); const { hasPermission } = usePermissions(); - // Get all controls from all requirements const allControls = frameworkInstanceWithControls.controls; const totalControls = allControls.length; - // Calculate compliant controls (all artifacts completed) const compliantControls = allControls.filter( - (control: any) => getControlStatus(control.policies, tasks, control.id) === 'completed', + (control) => getControlStatus( + control.policies, + tasks, + control.id, + control.controlDocumentTypes, + evidenceSubmissions, + ) === 'completed', ).length; - // Calculate compliance percentage based on compliant controls const compliancePercentage = totalControls > 0 ? Math.round((compliantControls / totalControls) * 100) : 0; - const getComplianceColor = (score: number) => { - if (score >= 80) return 'text-green-600 dark:text-green-400'; - if (score >= 60) return 'text-yellow-600 dark:text-yellow-400'; - return 'text-red-600 dark:text-red-400'; - }; - - const getComplianceBadgeVariant = () => { + const getComplianceBadgeVariant = (): 'default' | 'secondary' | 'destructive' => { if (compliancePercentage >= 80) return 'default'; if (compliancePercentage >= 60) return 'secondary'; return 'destructive'; @@ -61,109 +67,54 @@ export function FrameworkOverview({ return (
- {/* Framework Header */} -
-
-
-

- {frameworkInstanceWithControls.framework.name} -

- {compliancePercentage}% -
-

+ + + + + + { + setDropdownOpen(false); + setDeleteDialogOpen(true); + }} + className="text-destructive focus:text-destructive" + > + + Delete Framework + + + + ) : undefined + } + /> + {frameworkInstanceWithControls.framework.description && ( +

+ {frameworkInstanceWithControls.framework.description} -

+
- {hasPermission('framework', 'delete') && ( - - - - - - { - setDropdownOpen(false); - setDeleteDialogOpen(true); - }} - className="text-destructive focus:text-destructive" - > - - Delete Framework - - - - )} -
+ )} - {/* Compliance Dashboard */} -
- {/* Progress Card */} - - - - - Compliance Progress - - - -
-
-
- - {compliancePercentage} - - % complete -
- -
-
-
- {compliantControls} completed - {inProgressControls} remaining - {totalControls} total -
-
-
+
+ {compliancePercentage}% compliant + {compliantControls} completed + {inProgressControls} remaining + {totalControls} total controls +
- {/* Stats Card */} - - - - - Control Status - - - -
-
-
- Complete -
- {compliantControls} -
-
-
-
- In Progress -
- {inProgressControls} -
-
- Total - {totalControls} -
-
-
+
+
- {/* Delete Dialog */} setDeleteDialogOpen(false)} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx index d7fb8424c2..8e0b9008b3 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx @@ -1,7 +1,8 @@ 'use client'; -import type { FrameworkEditorRequirement } from '@db'; +import type { Control, FrameworkEditorRequirement, Task } from '@db'; import { + Badge, Heading, InputGroup, InputGroupAddon, @@ -17,18 +18,41 @@ import { import { Search } from '@trycompai/design-system/icons'; import { useParams, useRouter } from 'next/navigation'; import { useMemo, useState } from 'react'; -import type { FrameworkInstanceWithControls } from '../../types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; +import { getControlStatus } from '@/lib/control-compliance'; interface RequirementItem extends FrameworkEditorRequirement { mappedControlsCount: number; + satisfiedControlsCount: number; + compliancePercent: number; +} + +function getRequirementStatus( + satisfiedCount: number, + totalCount: number, +): { label: string; variant: 'default' | 'secondary' | 'destructive' } { + if (totalCount === 0) return { label: 'No Controls', variant: 'secondary' }; + if (satisfiedCount === totalCount) return { label: 'Satisfied', variant: 'default' }; + if (satisfiedCount > 0) return { label: 'In Progress', variant: 'secondary' }; + return { label: 'Not Started', variant: 'destructive' }; +} + +interface EvidenceSubmissionInfo { + id: string; + formType: string; + createdAt: Date | string; } export function FrameworkRequirements({ requirementDefinitions, frameworkInstanceWithControls, + tasks, + evidenceSubmissions = [], }: { requirementDefinitions: FrameworkEditorRequirement[]; frameworkInstanceWithControls: FrameworkInstanceWithControls; + tasks?: (Task & { controls: Control[] })[]; + evidenceSubmissions?: EvidenceSubmissionInfo[]; }) { const router = useRouter(); const { orgId, frameworkInstanceId } = useParams<{ @@ -39,17 +63,34 @@ export function FrameworkRequirements({ const items = useMemo(() => { return requirementDefinitions.map((def) => { - const mappedControlsCount = frameworkInstanceWithControls.controls.filter( + const mappedControls = frameworkInstanceWithControls.controls.filter( (control) => control.requirementsMapped?.some((reqMap) => reqMap.requirementId === def.id) ?? false, + ); + + const satisfiedControlsCount = mappedControls.filter( + (control) => getControlStatus( + control.policies, + tasks ?? [], + control.id, + control.controlDocumentTypes, + evidenceSubmissions, + ) === 'completed', ).length; + const compliancePercent = + mappedControls.length > 0 + ? Math.round((satisfiedControlsCount / mappedControls.length) * 100) + : 0; + return { ...def, - mappedControlsCount, + mappedControlsCount: mappedControls.length, + satisfiedControlsCount, + compliancePercent, }; }); - }, [requirementDefinitions, frameworkInstanceWithControls.controls]); + }, [requirementDefinitions, frameworkInstanceWithControls.controls, tasks, evidenceSubmissions]); const filteredItems = useMemo(() => { if (!searchTerm.trim()) return items; @@ -82,7 +123,7 @@ export function FrameworkRequirements({ setSearchTerm(event.target.value)} + onChange={(event: React.ChangeEvent) => setSearchTerm(event.target.value)} />
@@ -92,44 +133,75 @@ export function FrameworkRequirements({ Name Description Controls + Compliance + Status {filteredItems.length === 0 ? ( - + No requirements found. ) : ( - filteredItems.map((item) => ( - handleRowClick(item.id)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleRowClick(item.id); - } - }} - > - - {item.name} - - - {item.description} - - - - {item.mappedControlsCount} - - - - )) + filteredItems.map((item) => { + const status = getRequirementStatus(item.satisfiedControlsCount, item.mappedControlsCount); + + return ( + handleRowClick(item.id)} + onKeyDown={(event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleRowClick(item.id); + } + }} + style={{ cursor: 'pointer' }} + > + + {item.name} + + +
+ + {item.description} + +
+
+ +
+ + {item.satisfiedControlsCount}/{item.mappedControlsCount} + +
+
+ +
+
+
+
+
+ + {item.compliancePercent}% + +
+
+ + + {status.label} + + + ); + }) )} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/loading.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/loading.tsx new file mode 100644 index 0000000000..834c94b27f --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/loading.tsx @@ -0,0 +1,5 @@ +import { PageLayout } from '@trycompai/design-system'; + +export default function Loading() { + return ; +} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/page.tsx new file mode 100644 index 0000000000..8fc441d1df --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/controls/[controlId]/page.tsx @@ -0,0 +1,102 @@ +import { serverApi } from '@/lib/api-server'; +import { Breadcrumb, PageHeader, PageLayout } from '@trycompai/design-system'; +import type { + Control, + FrameworkEditorFramework, + FrameworkEditorRequirement, + FrameworkInstance, + Policy, + RequirementMap, + Task, +} from '@db'; +import Link from 'next/link'; +import { redirect } from 'next/navigation'; +import { SingleControl } from '@/app/(app)/[orgId]/controls/[controlId]/components/SingleControl'; +import type { ControlProgressResponse } from '@/app/(app)/[orgId]/controls/[controlId]/data/getOrganizationControlProgress'; + +type ControlDetail = Control & { + policies: Policy[]; + tasks: Task[]; + requirementsMapped: (RequirementMap & { + frameworkInstance: FrameworkInstance & { + framework: FrameworkEditorFramework; + }; + requirement: FrameworkEditorRequirement; + })[]; + progress: ControlProgressResponse; +}; + +interface PageProps { + params: Promise<{ + orgId: string; + frameworkInstanceId: string; + controlId: string; + }>; +} + +export default async function FrameworkControlPage({ params }: PageProps) { + const { orgId, frameworkInstanceId, controlId } = await params; + + const [controlRes, frameworkRes] = await Promise.all([ + serverApi.get(`/v1/controls/${controlId}`), + serverApi.get(`/v1/frameworks/${frameworkInstanceId}`), + ]); + + if (!controlRes.data || controlRes.error) { + redirect(`/${orgId}/frameworks/${frameworkInstanceId}`); + } + + const control = controlRes.data; + const frameworkName = frameworkRes.data?.framework?.name ?? 'Framework'; + const controlProgress: ControlProgressResponse = control.progress ?? { + total: 0, + completed: 0, + progress: 0, + byType: {}, + }; + + const matchedRequirement = control.requirementsMapped?.find( + (rm) => rm.frameworkInstanceId === frameworkInstanceId, + ); + const requirementName = matchedRequirement?.requirement?.name; + const requirementId = matchedRequirement?.requirement?.id; + const requirementHref = requirementId + ? `/${orgId}/frameworks/${frameworkInstanceId}/requirements/${requirementId}` + : undefined; + + const breadcrumbItems = [ + { + label: 'Frameworks', + href: `/${orgId}/frameworks`, + props: { render: }, + }, + { + label: frameworkName, + href: `/${orgId}/frameworks/${frameworkInstanceId}`, + props: { render: }, + }, + ...(requirementName && requirementHref + ? [ + { + label: requirementName, + href: requirementHref, + props: { render: }, + }, + ] + : []), + { label: control.name, isCurrent: true }, + ]; + + return ( + + + + + + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx index 1cdc95454b..3a0e393ff6 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/page.tsx @@ -1,6 +1,7 @@ import { serverApi } from '@/lib/api-server'; +import { Breadcrumb, PageHeader, PageLayout } from '@trycompai/design-system'; +import Link from 'next/link'; import { redirect } from 'next/navigation'; -import PageWithBreadcrumb from '../../../../../components/pages/PageWithBreadcrumb'; import { FrameworkOverview } from './components/FrameworkOverview'; import { FrameworkRequirements } from './components/FrameworkRequirements'; @@ -30,22 +31,28 @@ export default async function FrameworkPage({ params }: PageProps) { const frameworkName = framework.framework?.name ?? 'Framework'; return ( - -
- - -
-
+ + }, + }, + { label: frameworkName, isCurrent: true }, + ]} + /> + + + ); } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx index 1bde771dd5..cdfa99f8b7 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx @@ -1,45 +1,43 @@ 'use client'; -import type { Control, FrameworkEditorRequirement, RequirementMap, Task } from '@db'; +import type { Control, RequirementMap, Task } from '@db'; +import { Heading } from '@trycompai/design-system'; import { RequirementControlsTable } from './table/RequirementControlsTable'; +interface EvidenceSubmissionInfo { + id: string; + formType: string; + createdAt: Date | string; +} + interface RequirementControlsProps { - requirement: FrameworkEditorRequirement; tasks: (Task & { controls: Control[] })[]; - relatedControls: (RequirementMap & { control: Control })[]; + relatedControls: (RequirementMap & { control: Control & { policies: Array<{ id: string; name: string; status: string }> } } )[]; + evidenceSubmissions?: EvidenceSubmissionInfo[]; + frameworkInstanceId: string; } export function RequirementControls({ - requirement, tasks, relatedControls, + evidenceSubmissions = [], + frameworkInstanceId, }: RequirementControlsProps) { return ( -
- {/* Requirement Header */} -
-

{requirement.name}

- {requirement.description && ( -

{requirement.description}

- )} +
+
+ Controls + + {relatedControls.length} +
- {/* Controls Section */} -
-
-
-

Controls

- - {relatedControls.length} - -
-
- - control.control)} - tasks={tasks} - /> -
+ rc.control)} + tasks={tasks} + evidenceSubmissions={evidenceSubmissions} + frameworkInstanceId={frameworkInstanceId} + />
); } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx index 41db86e6b8..21984d0b8a 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx @@ -1,49 +1,67 @@ 'use client'; -import { DataTable } from '@/components/data-table/data-table'; -import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'; -import { useDataTable } from '@/hooks/use-data-table'; -import { Input } from '@trycompai/ui/input'; import type { Control, Task } from '@db'; -import { ColumnDef } from '@tanstack/react-table'; +import { + Badge, + InputGroup, + InputGroupAddon, + InputGroupInput, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Text, +} from '@trycompai/design-system'; +import { Launch, Search } from '@trycompai/design-system/icons'; import Link from 'next/link'; -import { useParams } from 'next/navigation'; +import { useParams, useRouter } from 'next/navigation'; import { useMemo, useState } from 'react'; +import { getControlStatus } from '@/lib/control-compliance'; + +type ControlWithPolicies = Control & { + policies?: Array<{ id: string; name: string; status: string }>; + controlDocumentTypes?: Array<{ formType: string }>; +}; + +interface EvidenceSubmissionInfo { + id: string; + formType: string; + createdAt: Date | string; +} interface RequirementControlsTableProps { - controls: Control[]; + controls: ControlWithPolicies[]; tasks: (Task & { controls: Control[] })[]; + evidenceSubmissions?: EvidenceSubmissionInfo[]; + frameworkInstanceId: string; } -export function RequirementControlsTable({ controls, tasks }: RequirementControlsTableProps) { +function getStatusBadge(status: string): { + label: string; + variant: 'default' | 'secondary' | 'destructive'; +} { + switch (status) { + case 'completed': + return { label: 'Satisfied', variant: 'default' }; + case 'in_progress': + return { label: 'In Progress', variant: 'secondary' }; + default: + return { label: 'Not Started', variant: 'destructive' }; + } +} + +export function RequirementControlsTable({ + controls, + tasks, + evidenceSubmissions = [], + frameworkInstanceId, +}: RequirementControlsTableProps) { const { orgId } = useParams<{ orgId: string }>(); + const router = useRouter(); const [searchTerm, setSearchTerm] = useState(''); - // Define columns for the controls table - const columns = useMemo[]>( - () => [ - { - id: 'name', - accessorKey: 'name', - header: ({ column }) => , - cell: ({ row }) => ( -
- - {row.original.name} - -
- ), - enableSorting: true, - size: 300, - minSize: 200, - maxSize: 400, - enableResizing: true, - }, - ], - [orgId], - ); - - // Filter controls data based on search term const filteredControls = useMemo(() => { if (!controls?.length) return []; if (!searchTerm.trim()) return controls; @@ -52,36 +70,116 @@ export function RequirementControlsTable({ controls, tasks }: RequirementControl return controls.filter((control) => control.name.toLowerCase().includes(searchLower)); }, [controls, searchTerm]); - // Set up the controls table - const table = useDataTable({ - data: filteredControls, - columns, - pageCount: 1, - shallow: false, - getRowId: (row) => row.id, - initialState: { - sorting: [{ id: 'name', desc: false }], - }, - }); + const getControlHref = (controlId: string) => + `/${orgId}/frameworks/${frameworkInstanceId}/controls/${controlId}`; + + const handleRowClick = (controlId: string) => { + router.push(getControlHref(controlId)); + }; + + if (!controls?.length) { + return ( +
+ No controls mapped to this requirement. +
+ ); + } return (
-
- setSearchTerm(e.target.value)} - className="max-w-sm" - /> - {/*
- -
*/} +
+ + + + + ) => setSearchTerm(e.target.value)} + /> +
- row.id} - /> + + + + + Control + Policies + Tasks + Status + + + + {filteredControls.length === 0 ? ( + + + + No controls found. + + + + ) : ( + filteredControls.map((control) => { + const controlTasks = tasks.filter((t) => t.controls.some((c) => c.id === control.id)); + const policies = control.policies ?? []; + const publishedCount = policies.filter((p) => p.status === 'published').length; + const doneTasks = controlTasks.filter( + (t) => t.status === 'done' || t.status === 'not_relevant', + ).length; + + const status = getControlStatus( + policies, + tasks, + control.id, + control.controlDocumentTypes, + evidenceSubmissions, + ); + const badge = getStatusBadge(status); + + return ( + handleRowClick(control.id)} + style={{ cursor: 'pointer' }} + > + + e.stopPropagation()} + className="group flex items-center gap-2" + > + + {control.name} + + + + + +
+ + {publishedCount}/{policies.length} + +
+
+ +
+ + {doneTasks}/{controlTasks.length} + +
+
+ + {badge.label} + +
+ ); + }) + )} +
+
); } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx deleted file mode 100644 index e64b437ac5..0000000000 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client'; - -import { StatusIndicator } from '@/components/status-indicator'; -import { isPolicyCompleted } from '@/lib/control-compliance'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip'; -import type { Control, Policy, Task } from '@db'; -import type { ColumnDef } from '@tanstack/react-table'; -import Link from 'next/link'; -import { useParams } from 'next/navigation'; -import { getControlStatus } from '../../../../../lib/utils'; - -export type OrganizationControlType = Control & { - policies: Policy[]; -}; - -export function RequirementControlsTableColumns({ - tasks, -}: { - tasks: (Task & { controls: Control[] })[]; -}): ColumnDef[] { - const { orgId } = useParams<{ orgId: string }>(); - - return [ - { - id: 'name', - accessorKey: 'name', - header: 'Control', - cell: ({ row }) => { - return ( -
- - {row.original.name} - -
- ); - }, - }, - { - id: 'status', - accessorKey: 'policies', - header: 'Status', - cell: ({ row }) => { - const controlData = row.original; - const policies = controlData.policies || []; - - const status = getControlStatus(policies, tasks, controlData.id); - - const totalPolicies = policies.length; - const completedPolicies = policies.filter(isPolicyCompleted).length; - - return ( - - - -
- -
-
- -
-

Progress: {Math.round((completedPolicies / totalPolicies) * 100) || 0}%

-

- Completed: {completedPolicies}/{totalPolicies} policies -

-
-
-
-
- ); - }, - }, - ]; -} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx index 5d1f78b284..cdde270f3a 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx @@ -1,5 +1,6 @@ -import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { serverApi } from '@/lib/api-server'; +import { Breadcrumb, PageHeader, PageLayout } from '@trycompai/design-system'; +import Link from 'next/link'; import { redirect } from 'next/navigation'; import { RequirementControls } from './components/RequirementControls'; @@ -31,40 +32,30 @@ export default async function RequirementPage({ params }: PageProps) { const frameworkName = framework.framework?.name ?? 'Framework'; const requirement = reqData.requirement; - const siblingRequirementsDropdown = (reqData.siblingRequirements ?? []).map( - (def: { id: string; name: string }) => ({ - label: def.name, - href: `/${organizationId}/frameworks/${frameworkInstanceId}/requirements/${def.id}`, - }), - ); - - const maxLabelLength = 40; - return ( - maxLabelLength - ? `${requirement.name.slice(0, maxLabelLength)}...` - : requirement.name, - dropdown: siblingRequirementsDropdown, - current: true, - }, - ]} - > -
- -
-
+ + }, + }, + { + label: frameworkName, + href: `/${organizationId}/frameworks/${frameworkInstanceId}`, + props: { render: }, + }, + { label: requirement.name, isCurrent: true }, + ]} + /> + + + ); } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksPageActions.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksPageActions.tsx new file mode 100644 index 0000000000..36d6c087b3 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksPageActions.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { Button } from '@trycompai/design-system'; +import { Add } from '@trycompai/design-system/icons'; +import { Dialog } from '@trycompai/ui/dialog'; +import type { FrameworkEditorFramework } from '@db'; +import { useState } from 'react'; +import { usePermissions } from '@/hooks/use-permissions'; +import { AddFrameworkModal } from '@/app/(app)/[orgId]/overview/components/AddFrameworkModal'; + +interface FrameworksPageActionsProps { + availableFrameworks: Pick< + FrameworkEditorFramework, + 'id' | 'name' | 'description' | 'version' | 'visible' + >[]; +} + +export function FrameworksPageActions({ availableFrameworks }: FrameworksPageActionsProps) { + const [isModalOpen, setIsModalOpen] = useState(false); + const { hasPermission } = usePermissions(); + + if (!hasPermission('framework', 'create')) { + return null; + } + + return ( + <> + + + {isModalOpen && ( + + )} + + + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksTable.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksTable.tsx new file mode 100644 index 0000000000..2a09d42508 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksTable.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { + Badge, + HStack, + InputGroup, + InputGroupAddon, + InputGroupInput, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Text, +} from '@trycompai/design-system'; +import { ArrowDown, ArrowUp, ArrowsVertical, Search } from '@trycompai/design-system/icons'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { useMemo, useState } from 'react'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; + +type SortColumn = 'name' | 'compliance' | 'controls'; +type SortDirection = 'asc' | 'desc'; + +interface FrameworksTableProps { + frameworks: FrameworkInstanceWithControls[]; + complianceMap: Record; + organizationId: string; +} + +const FRAMEWORK_BADGES: Record = { + 'SOC 2': '/badges/soc2.svg', + 'ISO 27001': '/badges/iso27001.svg', + 'ISO 42001': '/badges/iso42001.svg', + 'HIPAA': '/badges/hipaa.svg', + 'GDPR': '/badges/gdpr.svg', + 'PCI DSS': '/badges/pci-dss.svg', + 'NEN 7510': '/badges/nen7510.svg', + 'ISO 9001': '/badges/iso9001.svg', +}; + +function getFrameworkStatus(complianceScore: number): { + label: string; + variant: 'default' | 'secondary' | 'destructive'; +} { + if (complianceScore >= 100) return { label: 'Compliant', variant: 'default' }; + if (complianceScore > 0) return { label: 'In Progress', variant: 'secondary' }; + return { label: 'Not Started', variant: 'destructive' }; +} + +function SortIcon({ + column, + sortColumn, + sortDirection, +}: { + column: SortColumn; + sortColumn: SortColumn; + sortDirection: SortDirection; +}) { + if (sortColumn !== column) { + return ; + } + return sortDirection === 'asc' ? ( + + ) : ( + + ); +} + +export function FrameworksTable({ + frameworks, + complianceMap, + organizationId, +}: FrameworksTableProps) { + const router = useRouter(); + const [searchTerm, setSearchTerm] = useState(''); + const [sortColumn, setSortColumn] = useState('name'); + const [sortDirection, setSortDirection] = useState('asc'); + + const handleSort = (column: SortColumn) => { + if (sortColumn === column) { + setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); + } else { + setSortColumn(column); + setSortDirection('asc'); + } + }; + + const filteredAndSorted = useMemo(() => { + let items = [...frameworks]; + + if (searchTerm.trim()) { + const lower = searchTerm.toLowerCase(); + items = items.filter( + (fw) => + fw.framework.name.toLowerCase().includes(lower) || + fw.framework.description?.toLowerCase().includes(lower), + ); + } + + items.sort((a, b) => { + const dir = sortDirection === 'asc' ? 1 : -1; + switch (sortColumn) { + case 'name': + return dir * a.framework.name.localeCompare(b.framework.name); + case 'compliance': { + const scoreA = complianceMap[a.id] ?? 0; + const scoreB = complianceMap[b.id] ?? 0; + return dir * (scoreA - scoreB); + } + case 'controls': + return dir * (a.controls.length - b.controls.length); + default: + return 0; + } + }); + + return items; + }, [frameworks, searchTerm, sortColumn, sortDirection, complianceMap]); + + const handleRowClick = (frameworkId: string) => { + router.push(`/${organizationId}/frameworks/${frameworkId}`); + }; + + return ( +
+
+ + + + + ) => setSearchTerm(e.target.value)} + /> + +
+ + {filteredAndSorted.length === 0 ? ( +
+ No frameworks found. +
+ ) : ( + + + + + handleSort('name')} + > + Framework + + + + Description + + handleSort('compliance')} + > + Compliance + + + + + handleSort('controls')} + > + Controls + + + + Status + + + + {filteredAndSorted.map((fw) => { + const score = complianceMap[fw.id] ?? 0; + const roundedScore = Math.round(score); + const status = getFrameworkStatus(score); + const badgeSrc = FRAMEWORK_BADGES[fw.framework.name] ?? null; + + return ( + handleRowClick(fw.id)} + style={{ cursor: 'pointer' }} + > + + + {badgeSrc ? ( + + ) : ( +
+ + {fw.framework.name.charAt(0)} + +
+ )} + + {fw.framework.name} + +
+
+ +
+ + {fw.framework.description?.trim() || '—'} + +
+
+ +
+
+
+
+
+ + {roundedScore}% + +
+
+ + +
+ + {fw.controls.length} + +
+
+ + {status.label} + + + ); + })} + +
+ )} +
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts b/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts index 13cef3dadb..52ca2c9a38 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts @@ -2,7 +2,7 @@ import type { Control, PolicyStatus, RequirementMap } from '@db'; import { db } from '@db'; -import type { FrameworkInstanceWithControls } from '../types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; export async function getAllFrameworkInstancesWithControls({ organizationId, diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/data/getFrameworkWithComplianceScores.ts b/apps/app/src/app/(app)/[orgId]/frameworks/data/getFrameworkWithComplianceScores.ts index 88050f64fa..aabfdc7737 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/data/getFrameworkWithComplianceScores.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/data/getFrameworkWithComplianceScores.ts @@ -1,9 +1,9 @@ 'use server'; import { Control, type Task } from '@db'; -import { FrameworkInstanceWithComplianceScore } from '../components/types'; +import type { FrameworkInstanceWithComplianceScore } from '@/lib/types/framework'; import { computeFrameworkStats } from '../lib/compute'; -import { FrameworkInstanceWithControls } from '../types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; /** * Gets all framework instances for an organization with compliance calculations diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/compute.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/compute.ts index 64367d4845..402158e6c2 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/compute.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/compute.ts @@ -1,5 +1,5 @@ import type { Control, Task } from '@db'; -import type { FrameworkInstanceWithControls } from '../types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; import { countStrictlyCompletedTasks } from './taskEvidenceDocumentsScore'; export interface FrameworkStats { diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/utils.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/utils.ts deleted file mode 100644 index a87cd8cf0b..0000000000 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { StatusType } from '@/components/status-indicator'; -// Import base types explicitly -import type { Control, PolicyStatus } from '@db'; -import { Task } from '@db'; - -// Define the expected structure for policies passed to getControlStatus -// This should match the data structure provided by the calling code (e.g., from a Prisma select) -type SelectedPolicy = { - // Assuming at least status is present. Add other fields like id, name if available and needed. - status: PolicyStatus | null; // Allowing null status based on original ArtifactWithRelations -}; - -// Function to determine control status based on policies and tasks -export function getControlStatus( - policies: SelectedPolicy[], // Use the defined type for policies - tasks: (Task & { controls: Control[] })[], // tasks parameter seems fine - controlId: string, // controlId seems fine, used for filtering tasks -): StatusType { - const controlTasks = tasks.filter((task) => task.controls.some((c) => c.id === controlId)); - - // All policies are draft or none - const allPoliciesDraft = // Renamed from allArtifactsDraft - !policies.length || - policies.every( - (policy) => policy.status === 'draft', // Simplified from artifact.policy.status - ); - // All tasks are todo or none - const allTasksTodo = !controlTasks.length || controlTasks.every((task) => task.status === 'todo'); - - // All policies are published (and there are policies) AND all tasks are done (or no tasks) - const allPoliciesPublished = // Renamed from allArtifactsPublished - policies.length > 0 && - policies.every( - (policy) => policy.status === 'published', // Simplified from artifact.policy.status - ); - const allTasksDone = - controlTasks.length > 0 && controlTasks.every((task) => task.status === 'done' || task.status === 'not_relevant'); - - if (allPoliciesPublished && (controlTasks.length === 0 || allTasksDone)) return 'completed'; - if (allPoliciesDraft && allTasksTodo) return 'not_started'; - return 'in_progress'; -} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx index 22c68cde0b..37efdaef28 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx @@ -1,8 +1,9 @@ import { serverApi } from '@/lib/api-server'; -import type { FrameworkEditorFramework, Policy, Task } from '@db'; +import type { FrameworkEditorFramework } from '@db'; import { PageHeader, PageLayout } from '@trycompai/design-system'; -import { Overview, type FindingWithTarget } from './components/Overview'; -import type { FrameworkInstanceWithControls } from './types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; +import { FrameworksTable } from './components/FrameworksTable'; +import { FrameworksPageActions } from './components/FrameworksPageActions'; export async function generateMetadata() { return { title: 'Frameworks' }; @@ -10,76 +11,41 @@ export async function generateMetadata() { type FrameworkWithScore = FrameworkInstanceWithControls & { complianceScore: number }; -interface ScoresResponse { - policies: { - total: number; - published: number; - draftPolicies: Policy[]; - policiesInReview: Policy[]; - unpublishedPolicies: Policy[]; - }; - tasks: { - total: number; - done: number; - incompleteTasks: Task[]; - }; - people: { total: number; completed: number }; - documents: { totalDocuments: number; completedDocuments: number; outstandingDocuments: number }; - findings: FindingWithTarget[]; - onboardingTriggerJobId: string | null; - currentMember: { id: string; role: string } | null; -} - -export default async function DashboardPage({ params }: { params: Promise<{ orgId: string }> }) { +export default async function FrameworksPage({ params }: { params: Promise<{ orgId: string }> }) { const { orgId: organizationId } = await params; - const [scoresRes, frameworksRes, availableRes] = await Promise.all([ - serverApi.get('/v1/frameworks/scores'), + const [frameworksRes, availableRes] = await Promise.all([ serverApi.get<{ data: FrameworkWithScore[] }>('/v1/frameworks?includeControls=true&includeScores=true'), serverApi.get<{ data: FrameworkEditorFramework[] }>('/v1/frameworks/available'), ]); - const scores = scoresRes.data; const frameworksData = frameworksRes.data?.data ?? []; const allFrameworks = availableRes.data?.data ?? []; const frameworksWithControls = frameworksData.map(({ complianceScore: _, ...fw }) => fw); - const frameworksWithCompliance = frameworksData.map((fw) => ({ - frameworkInstance: { ...fw, complianceScore: undefined }, - complianceScore: fw.complianceScore ?? 0, - })); + const complianceMap = new Map( + frameworksData.map((fw) => [fw.id, fw.complianceScore ?? 0]), + ); + + const availableToAdd = allFrameworks.filter( + (framework) => !frameworksWithControls.some((fc) => fc.framework.id === framework.id), + ); return ( - }> - + } + /> + } + > + ); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/AddFrameworkModal.test.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/AddFrameworkModal.test.tsx similarity index 94% rename from apps/app/src/app/(app)/[orgId]/frameworks/components/AddFrameworkModal.test.tsx rename to apps/app/src/app/(app)/[orgId]/overview/components/AddFrameworkModal.test.tsx index 62f13772a4..6b91f1e796 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/AddFrameworkModal.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/components/AddFrameworkModal.test.tsx @@ -15,9 +15,8 @@ vi.mock('@/hooks/use-permissions', () => ({ }), })); -// Mock useFrameworks hook const mockAddFrameworks = vi.fn(); -vi.mock('../hooks/useFrameworks', () => ({ +vi.mock('@/hooks/use-frameworks', () => ({ useFrameworks: () => ({ addFrameworks: mockAddFrameworks, frameworks: [], @@ -28,12 +27,10 @@ vi.mock('../hooks/useFrameworks', () => ({ }), })); -// Mock sonner vi.mock('sonner', () => ({ toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() }, })); -// Mock @trycompai/ui components vi.mock('@trycompai/ui/button', () => ({ Button: ({ children, disabled, ...props }: any) => (
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx similarity index 98% rename from apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx rename to apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx index 47bf84f5d5..ec2eabe5b3 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx @@ -1,7 +1,7 @@ 'use client'; import { Finding, FrameworkEditorFramework, Policy, Task } from '@db'; -import { FrameworkInstanceWithControls } from '../types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; import { ComplianceOverview } from './ComplianceOverview'; import { FindingsOverview } from './FindingsOverview'; import { FrameworksOverview } from './FrameworksOverview'; diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/PeopleChart.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/PeopleChart.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/frameworks/components/PeopleChart.tsx rename to apps/app/src/app/(app)/[orgId]/overview/components/PeopleChart.tsx diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/PoliciesChart.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/PoliciesChart.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/frameworks/components/PoliciesChart.tsx rename to apps/app/src/app/(app)/[orgId]/overview/components/PoliciesChart.tsx diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/TasksChart.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/TasksChart.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/frameworks/components/TasksChart.tsx rename to apps/app/src/app/(app)/[orgId]/overview/components/TasksChart.tsx diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.test.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.test.tsx similarity index 97% rename from apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.test.tsx rename to apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.test.tsx index 035281ec4f..2710326d8b 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.test.tsx @@ -15,22 +15,18 @@ vi.mock('@/hooks/use-permissions', () => ({ }), })); -// Mock @trigger.dev/react-hooks vi.mock('@trigger.dev/react-hooks', () => ({ useRealtimeRun: () => ({ run: null }), })); -// Mock sonner vi.mock('sonner', () => ({ toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() }, })); -// Mock next/link vi.mock('next/link', () => ({ default: ({ children, href }: any) => {children}, })); -// Mock @trycompai/ui components vi.mock('@trycompai/ui/button', () => ({ Button: ({ children, disabled, asChild, ...props }: any) => ( +
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/overview/layout.tsx b/apps/app/src/app/(app)/[orgId]/overview/layout.tsx new file mode 100644 index 0000000000..0e791f9f5a --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/layout.tsx @@ -0,0 +1,14 @@ +import { requireRoutePermission } from '@/lib/permissions.server'; + +export default async function Layout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ orgId: string }>; +}) { + const { orgId } = await params; + await requireRoutePermission('overview', orgId); + + return children; +} diff --git a/apps/app/src/app/(app)/[orgId]/overview/loading.tsx b/apps/app/src/app/(app)/[orgId]/overview/loading.tsx new file mode 100644 index 0000000000..834c94b27f --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/loading.tsx @@ -0,0 +1,5 @@ +import { PageLayout } from '@trycompai/design-system'; + +export default function Loading() { + return ; +} diff --git a/apps/app/src/app/(app)/[orgId]/overview/page.tsx b/apps/app/src/app/(app)/[orgId]/overview/page.tsx new file mode 100644 index 0000000000..e6686ff8ca --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/page.tsx @@ -0,0 +1,88 @@ +import { serverApi } from '@/lib/api-server'; +import type { FrameworkEditorFramework, Policy, Task } from '@db'; +import { PageHeader, PageLayout } from '@trycompai/design-system'; +import { Overview, type FindingWithTarget } from './components/Overview'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; + +export async function generateMetadata() { + return { title: 'Overview' }; +} + +type FrameworkWithScore = FrameworkInstanceWithControls & { complianceScore: number }; + +interface ScoresResponse { + policies: { + total: number; + published: number; + draftPolicies: Policy[]; + policiesInReview: Policy[]; + unpublishedPolicies: Policy[]; + }; + tasks: { + total: number; + done: number; + incompleteTasks: Task[]; + }; + people: { total: number; completed: number }; + documents: { totalDocuments: number; completedDocuments: number; outstandingDocuments: number }; + findings: FindingWithTarget[]; + onboardingTriggerJobId: string | null; + currentMember: { id: string; role: string } | null; +} + +export default async function OverviewPage({ params }: { params: Promise<{ orgId: string }> }) { + const { orgId: organizationId } = await params; + + const [scoresRes, frameworksRes, availableRes] = await Promise.all([ + serverApi.get('/v1/frameworks/scores'), + serverApi.get<{ data: FrameworkWithScore[] }>('/v1/frameworks?includeControls=true&includeScores=true'), + serverApi.get<{ data: FrameworkEditorFramework[] }>('/v1/frameworks/available'), + ]); + + const scores = scoresRes.data; + const frameworksData = frameworksRes.data?.data ?? []; + const allFrameworks = availableRes.data?.data ?? []; + + const frameworksWithControls = frameworksData.map( + ({ complianceScore: _score, ...fw }: FrameworkWithScore) => fw, + ); + const frameworksWithCompliance = frameworksData.map((fw: FrameworkWithScore) => ({ + frameworkInstance: { ...fw, complianceScore: undefined }, + complianceScore: fw.complianceScore ?? 0, + })); + + return ( + }> + + + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyPageTabs.tsx b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyPageTabs.tsx index 6eee7dc0d2..ed311e64d9 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyPageTabs.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyPageTabs.tsx @@ -25,6 +25,33 @@ type PolicyVersionWithPublisher = PolicyVersion & { publishedBy: (Member & { user: User }) | null; }; +/** + * Normalizes policy content from the database into a clean JSONContent[]. + * Handles two known storage patterns: + * - { set: [...] } wrapper from a previous createMany bug + * - [{type:"doc", content:[...]}] where a full doc was stored as an array element + */ +function sanitizePolicyContent(raw: unknown): JSONContent[] { + if (!raw) return []; + + let arr: unknown[] = Array.isArray(raw) ? raw : [raw]; + + if (arr.length === 1 && arr[0] && typeof arr[0] === 'object' && !Array.isArray(arr[0])) { + const first = arr[0] as Record; + + // Unwrap { set: [...] } wrapper + if (Array.isArray(first.set)) { + arr = first.set; + } + // Unwrap [{type:"doc", content:[...]}] — extract the doc's children + else if (first.type === 'doc' && Array.isArray(first.content)) { + arr = first.content; + } + } + + return arr as JSONContent[]; +} + interface PolicyPageTabsProps { policy: (Policy & { approver: (Member & { user: User }) | null }) | null; assignees: (Member & { user: User })[]; @@ -181,19 +208,10 @@ export function PolicyPageTabs({ policyContent={ // Priority: 1) Published version content, 2) legacy policy.content, 3) empty array (() => { - // Ensure versions is an array before using find const versionsArray = Array.isArray(versions) ? versions : []; - // Find the published version content const currentVersion = versionsArray.find((v) => v.id === policy?.currentVersionId); - if (currentVersion?.content) { - const versionContent = currentVersion.content as JSONContent[]; - return Array.isArray(versionContent) ? versionContent : [versionContent]; - } - // Fallback to legacy policy.content for backward compatibility - if (policy?.content) { - return policy.content as JSONContent[]; - } - return []; + const raw = currentVersion?.content ?? policy?.content ?? []; + return sanitizePolicyContent(raw); })() } displayFormat={policy?.displayFormat} diff --git a/apps/app/src/app/(app)/[orgId]/settings/page.tsx b/apps/app/src/app/(app)/[orgId]/settings/page.tsx index 0a0e21c53e..3e80137595 100644 --- a/apps/app/src/app/(app)/[orgId]/settings/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/settings/page.tsx @@ -1,6 +1,5 @@ import { DeleteOrganization } from '@/components/forms/organization/delete-organization'; import { TransferOwnership } from '@/components/forms/organization/transfer-ownership'; -import { UpdateOrganizationAdvancedMode } from '@/components/forms/organization/update-organization-advanced-mode'; import { UpdateOrganizationLogo } from '@/components/forms/organization/update-organization-logo'; import { UpdateOrganizationName } from '@/components/forms/organization/update-organization-name'; import { UpdateOrganizationWebsite } from '@/components/forms/organization/update-organization-website'; @@ -20,7 +19,7 @@ export default async function OrganizationSettings({ const [orgBasic, res] = await Promise.all([ db.organization.findUnique({ where: { id: orgId }, - select: { id: true, name: true, website: true, advancedModeEnabled: true, logo: true }, + select: { id: true, name: true, website: true, logo: true }, }), serverApi.get<{ id: string; @@ -41,7 +40,6 @@ export default async function OrganizationSettings({ const organization = res.data; const orgName = organization?.name ?? orgBasic?.name ?? ''; const orgWebsite = organization?.website ?? orgBasic?.website ?? ''; - const advancedMode = organization?.advancedModeEnabled ?? orgBasic?.advancedModeEnabled ?? false; const logoUrl = organization?.logoUrl ?? null; return ( @@ -49,7 +47,6 @@ export default async function OrganizationSettings({ - ([]); const items: MenuItem[] = [ + { + id: 'overview', + path: '/:organizationId/overview', + name: 'Overview', + disabled: false, + icon: Gauge, + protected: false, + }, { id: 'frameworks', path: '/:organizationId/frameworks', - name: 'Overview', + name: 'Frameworks', disabled: false, icon: Gauge, protected: false, diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/hooks/useFrameworks.ts b/apps/app/src/hooks/use-frameworks.ts similarity index 94% rename from apps/app/src/app/(app)/[orgId]/frameworks/hooks/useFrameworks.ts rename to apps/app/src/hooks/use-frameworks.ts index fef91b92ed..11d291d42f 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/hooks/useFrameworks.ts +++ b/apps/app/src/hooks/use-frameworks.ts @@ -1,7 +1,7 @@ 'use client'; import { apiClient } from '@/lib/api-client'; -import type { FrameworkInstanceWithControls } from '../types'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; import useSWR from 'swr'; interface FrameworksApiResponse { @@ -54,19 +54,15 @@ export function useFrameworks(options?: UseFrameworksOptions) { const deleteFramework = async (id: string) => { const previous = frameworks; - - // Optimistic removal await mutate( frameworks.filter((f) => f.id !== id), false, ); - try { const response = await apiClient.delete(`/v1/frameworks/${id}`); if (response.error) throw new Error(response.error); await mutate(); } catch (err) { - // Rollback on error await mutate(previous, false); throw err; } diff --git a/apps/app/src/lib/control-compliance.ts b/apps/app/src/lib/control-compliance.ts index b87d7c6559..0844432d52 100644 --- a/apps/app/src/lib/control-compliance.ts +++ b/apps/app/src/lib/control-compliance.ts @@ -1,51 +1,97 @@ -import type { PolicyStatus } from '@db'; +import { StatusType } from '@/components/status-indicator'; +import type { Control } from '@db'; +import type { Task } from '@db'; -// Define the expected structure for policies (typically with selected fields) export type SelectedPolicy = { - // Add other fields like id, name if they are available and used by functions here - status: PolicyStatus | null; // Allowing null status as per original ArtifactWithRelations + status: string | null; }; -/** - * Checks if a specific policy is completed (e.g., published) - * @param policy - The policy to check - * @returns boolean indicating if the policy is completed - */ +export interface DocumentType { + formType: string; +} + +export interface EvidenceSubmissionInfo { + id: string; + formType: string; + createdAt: Date | string; +} + +const SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1000; + +export function getControlStatus( + policies: SelectedPolicy[], + tasks: (Task & { controls: Control[] })[], + controlId: string, + documentTypes?: DocumentType[], + evidenceSubmissions?: EvidenceSubmissionInfo[], +): StatusType { + const controlTasks = tasks.filter((task) => task.controls.some((c) => c.id === controlId)); + + const allPoliciesDraft = + !policies.length || policies.every((policy) => policy.status === 'draft'); + const allTasksTodo = + !controlTasks.length || controlTasks.every((task) => task.status === 'todo'); + + const allPoliciesPublished = + policies.length > 0 && policies.every((policy) => policy.status === 'published'); + const allTasksDone = + controlTasks.length > 0 && + controlTasks.every((task) => task.status === 'done' || task.status === 'not_relevant'); + + let allDocumentsFresh = true; + let hasDocumentRequirements = false; + let anyDocumentSubmitted = false; + + if (documentTypes?.length && evidenceSubmissions) { + hasDocumentRequirements = true; + const now = Date.now(); + + const sorted = [...evidenceSubmissions].sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + + for (const dt of documentTypes) { + const latestSubmission = sorted.find((es) => es.formType === dt.formType); + if (!latestSubmission) { + allDocumentsFresh = false; + continue; + } + anyDocumentSubmitted = true; + if (now - new Date(latestSubmission.createdAt).getTime() > SIX_MONTHS_MS) { + allDocumentsFresh = false; + } + } + } + + const policiesComplete = policies.length === 0 || allPoliciesPublished; + const tasksComplete = controlTasks.length === 0 || allTasksDone; + const documentsComplete = !hasDocumentRequirements || allDocumentsFresh; + + if (policiesComplete && tasksComplete && documentsComplete) { + const hasAnyArtifact = policies.length > 0 || controlTasks.length > 0 || hasDocumentRequirements; + if (hasAnyArtifact) return 'completed'; + } + + if (allPoliciesDraft && allTasksTodo && !anyDocumentSubmitted) return 'not_started'; + return 'in_progress'; +} + export function isPolicyCompleted(policy: SelectedPolicy): boolean { if (!policy) return false; return policy.status === 'published'; } -/** - * Determines if a control is compliant based on its policies - * @param policies - The control's policies - * @returns boolean indicating if the control is compliant - */ export function isControlCompliant(policies: SelectedPolicy[]): boolean { - if (!policies || policies.length === 0) { - return false; - } - - const totalPolicies = policies.length; - const completedPolicies = policies.filter(isPolicyCompleted).length; - - return completedPolicies === totalPolicies; + if (!policies || policies.length === 0) return false; + return policies.every(isPolicyCompleted); } -/** - * Calculate control status based on its policies - * @param policies - The control's policies - * @returns Control status as "not_started", "in_progress", or "completed" - */ export function calculateControlStatus( policies: SelectedPolicy[], ): 'not_started' | 'in_progress' | 'completed' { if (!policies || policies.length === 0) return 'not_started'; - - const totalPolicies = policies.length; const completedPolicies = policies.filter(isPolicyCompleted).length; - if (completedPolicies === 0) return 'not_started'; - if (completedPolicies === totalPolicies) return 'completed'; + if (completedPolicies === policies.length) return 'completed'; return 'in_progress'; } diff --git a/apps/app/src/lib/permissions.ts b/apps/app/src/lib/permissions.ts index f918cd84b7..348368e196 100644 --- a/apps/app/src/lib/permissions.ts +++ b/apps/app/src/lib/permissions.ts @@ -53,6 +53,7 @@ export function hasAnyPermission( */ export const ROUTE_PERMISSIONS: Record> = { // Main compliance pages + overview: [{ resource: 'framework', action: 'read' }], frameworks: [{ resource: 'framework', action: 'read' }], auditor: [{ resource: 'audit', action: 'read' }], controls: [{ resource: 'control', action: 'read' }], @@ -100,8 +101,8 @@ export function canAccessRoute(permissions: UserPermissions, routeSegment: strin * Order matches sidebar priority — the first accessible route becomes the default. */ const MAIN_NAV_ROUTES: Array<{ segment: string; path: string }> = [ + { segment: 'overview', path: '/overview' }, { segment: 'frameworks', path: '/frameworks' }, - { segment: 'controls', path: '/controls' }, { segment: 'policies', path: '/policies' }, { segment: 'tasks', path: '/tasks' }, { segment: 'people', path: '/people/all' }, @@ -139,7 +140,7 @@ const APP_IMPLYING_RESOURCES = new Set([ /** Compliance route segments — used to determine if the Compliance rail icon should show. */ const COMPLIANCE_ROUTE_SEGMENTS = [ - 'frameworks', 'controls', 'policies', 'tasks', 'documents', 'people', + 'overview', 'frameworks', 'controls', 'policies', 'tasks', 'documents', 'people', 'risk', 'vendors', 'questionnaire', 'integrations', 'cloud-tests', 'auditor', ] as const; diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/types.ts b/apps/app/src/lib/types/framework.ts similarity index 63% rename from apps/app/src/app/(app)/[orgId]/frameworks/types.ts rename to apps/app/src/lib/types/framework.ts index a50e40e388..e4e3e18c16 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/types.ts +++ b/apps/app/src/lib/types/framework.ts @@ -1,4 +1,4 @@ -import { +import type { Control, FrameworkEditorFramework, FrameworkInstance, @@ -15,5 +15,13 @@ export type FrameworkInstanceWithControls = FrameworkInstance & { status: PolicyStatus; }>; requirementsMapped: RequirementMap[]; + controlDocumentTypes?: Array<{ + formType: string; + }>; })[]; }; + +export interface FrameworkInstanceWithComplianceScore { + frameworkInstance: FrameworkInstanceWithControls; + complianceScore: number; +} diff --git a/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx b/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx index 05a2dbd1b2..b827a18683 100644 --- a/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx +++ b/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx @@ -1,13 +1,15 @@ 'use client'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -// import { db } from "@trycompai/db"; import { DataTable } from '@/app/components/DataTable'; import PageLayout from '@/app/components/PageLayout'; import type { FrameworkEditorFramework } from '@/db'; +import { Button } from '@trycompai/ui/button'; +import { Upload } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { columns } from './components/columns'; import { CreateFrameworkDialog } from './components/CreateFrameworkDialog'; +import { ImportFrameworkDialog } from './components/ImportFrameworkDialog'; export interface FrameworkWithCounts extends Omit { requirementsCount: number; @@ -20,6 +22,7 @@ interface FrameworksClientPageProps { export function FrameworksClientPage({ initialFrameworks }: FrameworksClientPageProps) { const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); const router = useRouter(); const handleRowClick = (framework: FrameworkWithCounts) => { @@ -35,12 +38,25 @@ export function FrameworksClientPage({ initialFrameworks }: FrameworksClientPage onCreateClick={() => setIsCreateDialogOpen(true)} createButtonLabel="Create New Framework" onRowClick={handleRowClick} + additionalActions={ + + } /> setIsCreateDialogOpen(false)} /> + ); } diff --git a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx index 41a0d8f922..4abb90d8ee 100644 --- a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx +++ b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx @@ -10,9 +10,10 @@ import { type SortingState, } from '@tanstack/react-table'; import { Button } from '@trycompai/ui'; -import { ArrowDown, ArrowUp, ArrowUpDown, PencilIcon, Plus, Trash2 } from 'lucide-react'; +import { ArrowDown, ArrowUp, ArrowUpDown, Download, PencilIcon, Plus, Trash2 } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useCallback, useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { DateCell, EditableCell, RelationalCell } from '../../../components/table'; import { EditFrameworkDialog } from './components/EditFrameworkDialog'; import { DeleteFrameworkDialog } from './components/DeleteFrameworkDialog'; @@ -216,6 +217,37 @@ export function FrameworkRequirementsClientPage({ }); }, [addRow]); + const [isExporting, setIsExporting] = useState(false); + + const handleExport = useCallback(async () => { + setIsExporting(true); + try { + const data = await apiClient>( + `/framework/${frameworkDetails.id}/export`, + ); + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + const safeName = frameworkDetails.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-'); + link.download = `${safeName}.json`; + link.click(); + URL.revokeObjectURL(url); + toast.success('Framework exported successfully'); + } catch (error) { + console.error('[ExportFramework] Error:', error); + const message = + error instanceof Error ? error.message : 'Failed to export framework'; + toast.error(message); + } finally { + setIsExporting(false); + } + }, [frameworkDetails.id, frameworkDetails.name]); + const handleFrameworkUpdated = () => { setIsEditDialogOpen(false); router.refresh(); @@ -238,6 +270,16 @@ export function FrameworkRequirementsClientPage({ )}
+ + + + + + + ); +} diff --git a/apps/framework-editor/app/components/DataTable.tsx b/apps/framework-editor/app/components/DataTable.tsx index 7d647e2f29..15bd8ef87b 100644 --- a/apps/framework-editor/app/components/DataTable.tsx +++ b/apps/framework-editor/app/components/DataTable.tsx @@ -93,12 +93,15 @@ export function DataTable({ {emptyMessage ?? 'Get started by creating your first entry.'}

- {onCreateClick && ( - - )} +
+ {additionalActions} + {onCreateClick && ( + + )} +
); } diff --git a/apps/framework-editor/app/layout.tsx b/apps/framework-editor/app/layout.tsx index 3b30d907fe..3d5672c63f 100644 --- a/apps/framework-editor/app/layout.tsx +++ b/apps/framework-editor/app/layout.tsx @@ -1,6 +1,7 @@ import { Toaster } from '@trycompai/ui/toaster'; import type { Metadata } from 'next'; import { type ReactNode } from 'react'; +import { Toaster as SonnerToaster } from 'sonner'; import { headers } from 'next/headers'; import '../styles/globals.css'; @@ -30,6 +31,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
{children} +
diff --git a/apps/framework-editor/prisma/schema.prisma b/apps/framework-editor/prisma/schema.prisma index 7e1e4522c3..c7f57a4879 100644 --- a/apps/framework-editor/prisma/schema.prisma +++ b/apps/framework-editor/prisma/schema.prisma @@ -599,6 +599,10 @@ model DynamicIntegration { /// Whether multiple connections per org are allowed supportsMultipleConnections Boolean @default(false) + /// Declarative sync definition (JSON — DSL steps that produce employee list) + /// When present and capabilities includes 'sync', enables employee sync + syncDefinition Json? + /// Whether this dynamic integration is active isActive Boolean @default(true) @@ -1314,7 +1318,7 @@ model IntegrationSyncLog { organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - /// Provider slug (e.g., "ramp", "google-workspace", "rippling", "jumpcloud") + /// Provider slug (e.g., "google-workspace", "rippling", "jumpcloud") provider String /// Event type (e.g., "employee_sync", "role_discovery", "role_mapping_save") eventType String diff --git a/bun.lock b/bun.lock index f3c491945a..3409c411cb 100644 --- a/bun.lock +++ b/bun.lock @@ -502,7 +502,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.5.2", - "electron": "33.4.0", + "electron": "35.7.5", "electron-builder": "^25.1.8", "electron-vite": "^3.1.0", "react": "^19.1.0", @@ -540,6 +540,23 @@ "react-dom": "^19.1.0", }, }, + "packages/framework-editor-cli": { + "name": "@trycompai/framework-editor-cli", + "version": "1.0.0", + "bin": { + "comp-framework": "./dist/index.js", + }, + "dependencies": { + "commander": "^13.1.0", + "conf": "^13.1.0", + "open": "^10.1.0", + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.5.0", + "typescript": "^5.8.3", + }, + }, "packages/integration-platform": { "name": "@trycompai/integration-platform", "version": "1.0.0", @@ -2426,6 +2443,8 @@ "@trycompai/framework-editor": ["@trycompai/framework-editor@workspace:apps/framework-editor"], + "@trycompai/framework-editor-cli": ["@trycompai/framework-editor-cli@workspace:packages/framework-editor-cli"], + "@trycompai/integration-platform": ["@trycompai/integration-platform@workspace:packages/integration-platform"], "@trycompai/integrations": ["@trycompai/integrations@workspace:packages/integrations"], @@ -3308,7 +3327,7 @@ "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], - "conf": ["conf@14.0.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.4.0" } }, "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw=="], + "conf": ["conf@13.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.6.3", "uint8array-extras": "^1.4.0" } }, "sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], @@ -3638,7 +3657,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron": ["electron@33.4.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-AO+Q/ygWwKKs+JtNEFgfS5ntjG3TA2HX7s4IEbiYi6lktaocuLP2oScG1/mmKRuUWoOcow2RRsf995L2mM4bvQ=="], + "electron": ["electron@35.7.5", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw=="], "electron-builder": ["electron-builder@25.1.8", "", { "dependencies": { "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", "dmg-builder": "25.1.8", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig=="], @@ -3692,7 +3711,7 @@ "env-ci": ["env-ci@11.2.0", "", { "dependencies": { "execa": "^8.0.0", "java-properties": "^1.0.2" } }, "sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA=="], - "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -6552,6 +6571,8 @@ "@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -6880,6 +6901,8 @@ "@trycompai/framework-editor/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@trycompai/framework-editor-cli/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + "@trycompai/portal/better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], "@trycompai/portal/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -7038,12 +7061,12 @@ "conf/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "config-file-ts/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "cosmiconfig/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], @@ -7084,7 +7107,7 @@ "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + "electron/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -7096,6 +7119,8 @@ "electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "electron-store/conf": ["conf@14.0.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.4.0" } }, "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw=="], + "electron-updater/builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -8138,6 +8163,8 @@ "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "@electron/rebuild/node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "@electron/rebuild/node-gyp/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@electron/rebuild/node-gyp/nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], @@ -8478,6 +8505,8 @@ "@trycompai/email/resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], + "@trycompai/framework-editor-cli/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@trycompai/framework-editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@trycompai/framework-editor/better-auth/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], @@ -8646,6 +8675,8 @@ "electron-builder-squirrel-windows/archiver/zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + "electron-store/conf/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "electron-updater/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "electron-vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -9468,6 +9499,8 @@ "electron-builder-squirrel-windows/archiver/zip-stream/compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="], + "electron-store/conf/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "env-ci/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "env-ci/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 8223e39d10..ac49c7243a 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -8343,6 +8343,37 @@ ] } }, + "/v1/tasks/templates": { + "get": { + "description": "Retrieve all available task templates, optionally filtered by framework.", + "operationId": "TasksController_getTaskTemplates_v1", + "parameters": [ + { + "name": "frameworkId", + "required": false, + "in": "query", + "description": "Filter templates by framework ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Get task templates", + "tags": [ + "Tasks" + ] + } + }, "/v1/tasks/bulk": { "patch": { "description": "Bulk update the status of multiple tasks", @@ -12681,6 +12712,53 @@ ] } }, + "/v1/framework-editor/framework/import": { + "post": { + "operationId": "FrameworkEditorFrameworkController_importFramework_v1", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportFrameworkDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Framework Editor Frameworks" + ] + } + }, + "/v1/framework-editor/framework/{id}/export": { + "get": { + "operationId": "FrameworkEditorFrameworkController_exportFramework_v1", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Framework Editor Frameworks" + ] + } + }, "/v1/framework-editor/framework/{id}/controls": { "get": { "operationId": "FrameworkEditorFrameworkController_getControls_v1", @@ -14327,17 +14405,17 @@ }, "responses": { "200": { - "description": "Upload file, parse questions (no answers), save to DB, return questionnaireId", + "description": "Upload file and trigger async parsing. Returns runId for realtime tracking.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "questionnaireId": { + "runId": { "type": "string" }, - "totalQuestions": { - "type": "number" + "publicAccessToken": { + "type": "string" } } } @@ -15997,6 +16075,66 @@ ] } }, + "/v1/internal/dynamic-integrations/validate": { + "post": { + "operationId": "DynamicIntegrationsController_validate_v1", + "parameters": [], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "DynamicIntegrations" + ] + } + }, + "/v1/internal/dynamic-integrations/{id}/check-runs": { + "get": { + "operationId": "DynamicIntegrationsController_getCheckRuns_v1", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "DynamicIntegrations" + ] + } + }, + "/v1/internal/dynamic-integrations/check-runs/{runId}": { + "get": { + "operationId": "DynamicIntegrationsController_getCheckRunById_v1", + "parameters": [ + { + "name": "runId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "DynamicIntegrations" + ] + } + }, "/v1/integrations/checks/providers/{providerSlug}": { "get": { "operationId": "ChecksController_listProviderChecks_v1", @@ -16480,34 +16618,6 @@ ] } }, - "/v1/integrations/sync/ramp/employees": { - "post": { - "operationId": "SyncController_syncRampEmployees_v1", - "parameters": [ - { - "name": "connectionId", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "apikey": [] - } - ], - "tags": [ - "Integrations" - ] - } - }, "/v1/integrations/sync/jumpcloud/employees": { "post": { "operationId": "SyncController_syncJumpCloudEmployees_v1", @@ -16555,25 +16665,6 @@ ] } }, - "/v1/integrations/sync/ramp/status": { - "post": { - "operationId": "SyncController_getRampStatus_v1", - "parameters": [], - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "apikey": [] - } - ], - "tags": [ - "Integrations" - ] - } - }, "/v1/integrations/sync/employee-sync-provider": { "get": { "operationId": "SyncController_getEmployeeSyncProvider_v1", @@ -16610,29 +16701,12 @@ ] } }, - "/v1/integrations/sync/ramp/discover-roles": { - "post": { - "operationId": "RampRoleMappingController_discoverRoles_v1", - "parameters": [ - { - "name": "connectionId", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "refresh", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "/v1/integrations/sync/available-providers": { + "get": { + "operationId": "SyncController_getAvailableSyncProviders_v1", + "parameters": [], "responses": { - "201": { + "200": { "description": "" } }, @@ -16646,27 +16720,18 @@ ] } }, - "/v1/integrations/sync/ramp/role-mapping": { + "/v1/integrations/sync/dynamic/{providerSlug}/employees": { "post": { - "operationId": "RampRoleMappingController_saveRoleMapping_v1", - "parameters": [], - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "apikey": [] - } - ], - "tags": [ - "Integrations" - ] - }, - "get": { - "operationId": "RampRoleMappingController_getRoleMapping_v1", + "operationId": "SyncController_syncDynamicProviderEmployees_v1", "parameters": [ + { + "name": "providerSlug", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, { "name": "connectionId", "required": true, @@ -16677,7 +16742,7 @@ } ], "responses": { - "200": { + "201": { "description": "" } }, @@ -21840,13 +21905,11 @@ "properties": { "name": { "type": "string", - "description": "Vendor name", - "example": "CloudTech Solutions Inc." + "description": "Vendor name" }, "description": { "type": "string", - "description": "Detailed description of the vendor and services provided", - "example": "Cloud infrastructure provider offering AWS-like services including compute, storage, and networking solutions for enterprise customers." + "description": "Vendor description" }, "category": { "type": "string", @@ -21860,87 +21923,72 @@ "sales", "hr", "other" - ], - "default": "other", - "example": "cloud" + ] }, "status": { "type": "string", - "description": "Assessment status of the vendor", + "description": "Assessment status", "enum": [ "not_assessed", "in_progress", "assessed" - ], - "default": "not_assessed", - "example": "not_assessed" + ] }, "inherentProbability": { "type": "string", - "description": "Inherent probability of risk before controls", + "description": "Inherent probability", "enum": [ "very_unlikely", "unlikely", "possible", "likely", "very_likely" - ], - "default": "very_unlikely", - "example": "possible" + ] }, "inherentImpact": { "type": "string", - "description": "Inherent impact of risk before controls", + "description": "Inherent impact", "enum": [ "insignificant", "minor", "moderate", "major", "severe" - ], - "default": "insignificant", - "example": "moderate" + ] }, "residualProbability": { "type": "string", - "description": "Residual probability after controls are applied", + "description": "Residual probability", "enum": [ "very_unlikely", "unlikely", "possible", "likely", "very_likely" - ], - "default": "very_unlikely", - "example": "unlikely" + ] }, "residualImpact": { "type": "string", - "description": "Residual impact after controls are applied", + "description": "Residual impact", "enum": [ "insignificant", "minor", "moderate", "major", "severe" - ], - "default": "insignificant", - "example": "minor" + ] }, "website": { "type": "string", - "description": "Vendor website URL", - "example": "https://www.cloudtechsolutions.com" + "description": "Vendor website URL" }, "isSubProcessor": { "type": "boolean", - "description": "Whether the vendor is a sub-processor", - "default": false + "description": "Whether the vendor is a sub-processor" }, "assigneeId": { "type": "string", - "description": "ID of the user assigned to manage this vendor", - "example": "mem_abc123def456" + "description": "Assignee member ID" } } }, @@ -24107,6 +24155,71 @@ "description" ] }, + "ImportFrameworkMetaDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "visible": { + "type": "boolean" + } + }, + "required": [ + "name", + "version", + "description" + ] + }, + "ImportFrameworkDto": { + "type": "object", + "properties": { + "version": { + "type": "string", + "example": "1" + }, + "exportedAt": { + "type": "string" + }, + "framework": { + "$ref": "#/components/schemas/ImportFrameworkMetaDto" + }, + "requirements": { + "type": "array", + "items": { + "type": "string" + } + }, + "controlTemplates": { + "type": "array", + "items": { + "type": "string" + } + }, + "policyTemplates": { + "type": "array", + "items": { + "type": "string" + } + }, + "taskTemplates": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "version", + "framework" + ] + }, "UpdateFrameworkDto": { "type": "object", "properties": { diff --git a/packages/framework-editor-cli/package.json b/packages/framework-editor-cli/package.json new file mode 100644 index 0000000000..f0d69188c5 --- /dev/null +++ b/packages/framework-editor-cli/package.json @@ -0,0 +1,30 @@ +{ + "name": "@trycompai/framework-editor-cli", + "description": "CLI for managing framework editor templates via the Comp AI API", + "version": "1.0.0", + "private": true, + "type": "module", + "bin": { + "comp-framework": "./dist/index.js" + }, + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "rm -rf .turbo node_modules dist", + "typecheck": "tsc --noEmit", + "lint": "prettier --check ." + }, + "dependencies": { + "commander": "^13.1.0", + "conf": "^13.1.0", + "open": "^10.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.5.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/framework-editor-cli/src/commands/auth.ts b/packages/framework-editor-cli/src/commands/auth.ts new file mode 100644 index 0000000000..79e49bf8c5 --- /dev/null +++ b/packages/framework-editor-cli/src/commands/auth.ts @@ -0,0 +1,216 @@ +import { Command } from 'commander'; +import { createServer, type Server } from 'node:http'; +import { randomBytes } from 'node:crypto'; +import open from 'open'; +import { + getApiUrl, + getPortalUrl, + saveCredentials, + clearCredentials, + getStoredCredentials, + getSessionToken, +} from '../lib/config.js'; +import { rawApiRequest } from '../lib/api-client.js'; +import { handleError, CliError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; + +const LOGIN_TIMEOUT_MS = 5 * 60 * 1000; + +export function registerAuthCommands(parent: Command): void { + const auth = parent + .command('auth') + .description('Manage authentication. Login via browser to obtain a session token.'); + + auth + .command('login') + .description( + 'Authenticate with the Comp AI platform. Opens a browser window for login, ' + + 'then stores the session token locally in an encrypted config file. ' + + 'Requires platform admin privileges (user.role = admin).', + ) + .action(async (_opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await loginAction(cmd.optsWithGlobals().apiUrl as string | undefined, json); + } catch (error) { + handleError(error, json); + } + }); + + auth + .command('status') + .description( + 'Check current authentication status. Validates the stored session token ' + + 'against the API and displays user info if authenticated.', + ) + .action(async (_opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await statusAction(cmd.optsWithGlobals().apiUrl as string | undefined, json); + } catch (error) { + handleError(error, json); + } + }); + + auth + .command('logout') + .description('Clear stored credentials and session token from the local config.') + .action((_opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + clearCredentials(); + outputSuccess('Logged out. Stored credentials have been cleared.', { json }); + }); +} + +async function loginAction(apiUrlOverride: string | undefined, json: boolean): Promise { + const apiUrl = getApiUrl(apiUrlOverride); + const portalUrl = getPortalUrl(apiUrl); + const state = randomBytes(16).toString('hex'); + + let server: Server | null = null; + + try { + const { port, codePromise, serverInstance } = await startCallbackServer(state); + server = serverInstance; + + const authUrl = `${portalUrl}/auth?device_auth=true&callback_port=${port}&state=${state}`; + + if (!json) { + console.log('Opening browser for login...'); + console.log(`If the browser does not open, visit: ${authUrl}`); + } + + await open(authUrl); + + const code = await codePromise; + if (!code) { + throw new CliError('Login timed out or was cancelled. Please try again.'); + } + + const exchangeUrl = `${apiUrl}/v1/device-agent/exchange-code`; + const response = await fetch(exchangeUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }), + }); + + if (!response.ok) { + throw new CliError(`Code exchange failed (${response.status}). Please try again.`); + } + + const { session_token, user_id } = (await response.json()) as { + session_token: string; + user_id: string; + }; + + saveCredentials(session_token, user_id, apiUrl); + outputSuccess(`Authenticated successfully (userId: ${user_id}).`, { json }); + } finally { + if (server) { + server.closeAllConnections(); + server.close(); + } + } +} + +async function statusAction(apiUrlOverride: string | undefined, json: boolean): Promise { + const token = getSessionToken(); + if (!token) { + throw new CliError('Not authenticated. Run `comp-framework auth login` first.'); + } + + const stored = getStoredCredentials(); + const apiUrl = getApiUrl(apiUrlOverride); + + try { + const session = await rawApiRequest<{ + user?: { id: string; email: string; name: string; role: string }; + }>('/api/auth/get-session', { apiUrl }); + + outputResult( + { + authenticated: true, + user: session.user ?? null, + apiUrl: stored?.apiUrl ?? apiUrl, + source: process.env['COMP_SESSION_TOKEN'] ? 'environment' : 'config', + }, + { json }, + ); + } catch { + throw new CliError( + 'Session is invalid or expired. Run `comp-framework auth login` to re-authenticate.', + ); + } +} + +function startCallbackServer( + expectedState: string, +): Promise<{ port: number; codePromise: Promise; serverInstance: Server }> { + return new Promise((resolveSetup, rejectSetup) => { + let resolveCode: (code: string | null) => void; + let timeoutId: NodeJS.Timeout; + + const codePromise = new Promise((resolve) => { + resolveCode = (value) => { + clearTimeout(timeoutId); + resolve(value); + }; + }); + + const server = createServer((req, res) => { + res.setHeader('Connection', 'close'); + const url = new URL(req.url ?? '/', 'http://localhost'); + + if (url.pathname !== '/auth-callback') { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); + return; + } + + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + + if (state !== expectedState) { + res.writeHead(400, { 'Content-Type': 'text/html' }); + res.end(htmlPage('Sign-in failed', 'Invalid state parameter. Please try again.')); + resolveCode(null); + return; + } + + if (!code) { + res.writeHead(400, { 'Content-Type': 'text/html' }); + res.end(htmlPage('Sign-in failed', 'Missing authorization code. Please try again.')); + resolveCode(null); + return; + } + + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end( + htmlPage('Sign-in complete!', 'You can close this tab and return to the terminal.'), + ); + resolveCode(code); + }); + + server.listen(0, '127.0.0.1', () => { + const addr = server.address(); + if (!addr || typeof addr === 'string') { + rejectSetup(new Error('Failed to get server address')); + return; + } + resolveSetup({ port: addr.port, codePromise, serverInstance: server }); + }); + + server.on('error', (err) => rejectSetup(err)); + + timeoutId = setTimeout(() => resolveCode(null), LOGIN_TIMEOUT_MS); + }); +} + +function htmlPage(title: string, message: string): string { + return `Comp AI CLI + +
+

${title}

+

${message}

+
`; +} diff --git a/packages/framework-editor-cli/src/commands/control-relations.ts b/packages/framework-editor-cli/src/commands/control-relations.ts new file mode 100644 index 0000000000..f2f23d9e96 --- /dev/null +++ b/packages/framework-editor-cli/src/commands/control-relations.ts @@ -0,0 +1,184 @@ +import type { Command } from 'commander'; +import { apiRequest } from '../lib/api-client.js'; +import { handleError, CliError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; +import type { ControlTemplate } from '../types.js'; +import { EVIDENCE_FORM_TYPE_VALUES } from '../types.js'; + +export function registerControlRelationCommands(ctl: Command): void { + ctl + .command('link-requirement') + .argument('', 'Control template ID') + .argument('', 'Requirement ID to link') + .description('Link a requirement to this control template (many-to-many relationship).') + .action(async (controlId: string, requirementId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${controlId}/requirements/${requirementId}`, { + method: 'POST', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Requirement ${requirementId} linked to control ${controlId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('unlink-requirement') + .argument('', 'Control template ID') + .argument('', 'Requirement ID to unlink') + .description('Remove the link between a requirement and this control template.') + .action(async (controlId: string, requirementId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${controlId}/requirements/${requirementId}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Requirement ${requirementId} unlinked from control ${controlId}.`, { + json, + }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('link-policy') + .argument('', 'Control template ID') + .argument('', 'Policy template ID to link') + .description('Link a policy template to this control (many-to-many relationship).') + .action(async (controlId: string, policyId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${controlId}/policy-templates/${policyId}`, { + method: 'POST', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Policy ${policyId} linked to control ${controlId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('unlink-policy') + .argument('', 'Control template ID') + .argument('', 'Policy template ID to unlink') + .description('Remove the link between a policy template and this control.') + .action(async (controlId: string, policyId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${controlId}/policy-templates/${policyId}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Policy ${policyId} unlinked from control ${controlId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('link-task') + .argument('', 'Control template ID') + .argument('', 'Task template ID to link') + .description('Link a task template to this control (many-to-many relationship).') + .action(async (controlId: string, taskId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${controlId}/task-templates/${taskId}`, { + method: 'POST', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Task ${taskId} linked to control ${controlId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('unlink-task') + .argument('', 'Control template ID') + .argument('', 'Task template ID to unlink') + .description('Remove the link between a task template and this control.') + .action(async (controlId: string, taskId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${controlId}/task-templates/${taskId}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Task ${taskId} unlinked from control ${controlId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('add-document-type') + .argument('', 'Control template ID') + .argument('', `Document type to add (${EVIDENCE_FORM_TYPE_VALUES.join(', ')})`) + .description( + 'Add an evidence document type to a control template. This is an atomic operation ' + + 'that appends to the existing list without replacing it. ' + + 'Use "framework documents " to see the current matrix.', + ) + .action(async (controlId: string, documentType: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + if (!EVIDENCE_FORM_TYPE_VALUES.includes(documentType as never)) { + throw new CliError( + `Invalid document type: "${documentType}". Must be one of: ${EVIDENCE_FORM_TYPE_VALUES.join(', ')}`, + ); + } + const current = await apiRequest(`/control-template/${controlId}`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + const types = Array.isArray(current.documentTypes) ? current.documentTypes : []; + if (types.includes(documentType)) { + outputSuccess(`Document type "${documentType}" already exists on control ${controlId}.`, { json }); + return; + } + const data = await apiRequest(`/control-template/${controlId}`, { + method: 'PATCH', + body: { documentTypes: [...types, documentType] }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('remove-document-type') + .argument('', 'Control template ID') + .argument('', `Document type to remove (${EVIDENCE_FORM_TYPE_VALUES.join(', ')})`) + .description( + 'Remove an evidence document type from a control template. This is an atomic ' + + 'operation that removes only the specified type without affecting others.', + ) + .action(async (controlId: string, documentType: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const current = await apiRequest(`/control-template/${controlId}`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + const types = Array.isArray(current.documentTypes) ? current.documentTypes : []; + if (!types.includes(documentType)) { + outputSuccess(`Document type "${documentType}" not found on control ${controlId}.`, { json }); + return; + } + const data = await apiRequest(`/control-template/${controlId}`, { + method: 'PATCH', + body: { documentTypes: types.filter((t) => t !== documentType) }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); +} diff --git a/packages/framework-editor-cli/src/commands/control.ts b/packages/framework-editor-cli/src/commands/control.ts new file mode 100644 index 0000000000..9eb7906c6c --- /dev/null +++ b/packages/framework-editor-cli/src/commands/control.ts @@ -0,0 +1,150 @@ +import { Command } from 'commander'; +import { apiRequest } from '../lib/api-client.js'; +import { handleError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; +import type { ControlTemplate } from '../types.js'; +import { EVIDENCE_FORM_TYPE_VALUES } from '../types.js'; +import { registerControlRelationCommands } from './control-relations.js'; + +export function registerControlCommands(parent: Command): void { + const ctl = parent + .command('control') + .alias('ctl') + .description( + 'Manage control templates. Controls are security/compliance measures that satisfy ' + + 'framework requirements. Controls can link to policies, tasks, requirements, ' + + 'and evidence document types.', + ); + + ctl + .command('list') + .description( + 'List control templates. Optionally filter by framework. ' + + 'Returns id, name, description, and document types.', + ) + .option('--framework-id ', 'Filter controls by framework ID') + .option('--take ', 'Maximum number of results (default: 100)', '100') + .option('--skip ', 'Number of results to skip (default: 0)', '0') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/control-template', { + query: { + frameworkId: opts.frameworkId, + take: opts.take, + skip: opts.skip, + }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('get') + .argument('', 'Control template ID') + .description( + 'Get a single control template by ID. Returns full details including ' + + 'linked requirements, policies, tasks, and document types.', + ) + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest(`/control-template/${id}`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('create') + .description( + 'Create a new control template. If --framework-id is provided, the control ' + + 'is automatically linked to all requirements of that framework.', + ) + .requiredOption('--name ', 'Control name (e.g. "Access Control Policy")') + .requiredOption('--description ', 'Control description') + .option( + '--document-types ', + `Comma-separated evidence document types. Valid values: ${EVIDENCE_FORM_TYPE_VALUES.join(', ')}`, + ) + .option( + '--framework-id ', + 'Framework ID. When set, auto-links the control to all requirements in that framework.', + ) + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const body: Record = { + name: opts.name, + description: opts.description, + }; + if (opts.documentTypes) { + body.documentTypes = (opts.documentTypes as string).split(',').map((s: string) => s.trim()); + } + const data = await apiRequest('/control-template', { + method: 'POST', + body, + query: { frameworkId: opts.frameworkId }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('update') + .argument('', 'Control template ID to update') + .description('Update a control template. Only provided fields are changed.') + .option('--name ', 'New control name') + .option('--description ', 'New description') + .option( + '--document-types ', + `Comma-separated evidence document types. Valid values: ${EVIDENCE_FORM_TYPE_VALUES.join(', ')}`, + ) + .action(async (id: string, opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const body: Record = {}; + if (opts.name !== undefined) body.name = opts.name; + if (opts.description !== undefined) body.description = opts.description; + if (opts.documentTypes) { + body.documentTypes = (opts.documentTypes as string).split(',').map((s: string) => s.trim()); + } + const data = await apiRequest(`/control-template/${id}`, { + method: 'PATCH', + body, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + ctl + .command('delete') + .argument('', 'Control template ID to delete') + .description('Delete a control template permanently. This cannot be undone.') + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/control-template/${id}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Control template ${id} deleted.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + registerControlRelationCommands(ctl); +} diff --git a/packages/framework-editor-cli/src/commands/framework.ts b/packages/framework-editor-cli/src/commands/framework.ts new file mode 100644 index 0000000000..5802fda6f4 --- /dev/null +++ b/packages/framework-editor-cli/src/commands/framework.ts @@ -0,0 +1,208 @@ +import { Command } from 'commander'; +import { apiRequest } from '../lib/api-client.js'; +import { handleError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; +import type { Framework, FrameworkWithCounts, ControlDocument } from '../types.js'; + +export function registerFrameworkCommands(parent: Command): void { + const fw = parent + .command('framework') + .alias('fw') + .description( + 'Manage framework templates. Frameworks are top-level compliance standards ' + + '(e.g. SOC 2, ISO 27001) that contain requirements, controls, policies, and tasks.', + ); + + fw.command('list') + .description( + 'List all framework templates. Returns id, name, version, description, and visibility status.', + ) + .option('--take ', 'Maximum number of results to return (default: 100)', '100') + .option('--skip ', 'Number of results to skip for pagination (default: 0)', '0') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/framework', { + query: { take: opts.take, skip: opts.skip }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('get') + .argument('', 'Framework ID (e.g. "fe_cm...")') + .description( + 'Get a single framework by ID. Returns full details including nested requirements ' + + 'with their linked control templates.', + ) + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest(`/framework/${id}`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('create') + .description( + 'Create a new framework template. Requires name, version, and description. ' + + 'The framework is hidden (visible=false) by default until explicitly made visible.', + ) + .requiredOption('--name ', 'Framework name (e.g. "SOC 2 Type II")') + .requiredOption('--version ', 'Framework version (e.g. "2024")') + .requiredOption('--description ', 'Framework description') + .option('--visible', 'Make the framework visible to organizations immediately', false) + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/framework', { + method: 'POST', + body: { + name: opts.name, + version: opts.version, + description: opts.description, + visible: opts.visible, + }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('update') + .argument('', 'Framework ID to update') + .description( + 'Update a framework template. All fields are optional; only provided fields are changed.', + ) + .option('--name ', 'New framework name') + .option('--version ', 'New framework version') + .option('--description ', 'New framework description') + .option('--visible', 'Make the framework visible') + .option('--no-visible', 'Hide the framework') + .action(async (id: string, opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const body: Record = {}; + if (opts.name !== undefined) body.name = opts.name; + if (opts.version !== undefined) body.version = opts.version; + if (opts.description !== undefined) body.description = opts.description; + if (opts.visible !== undefined) body.visible = opts.visible; + const data = await apiRequest(`/framework/${id}`, { + method: 'PATCH', + body, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('delete') + .argument('', 'Framework ID to delete') + .description('Delete a framework template permanently. This cannot be undone.') + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/framework/${id}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Framework ${id} deleted.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('documents') + .argument('', 'Framework ID') + .description( + 'List the document type matrix for a framework. Shows each control template ' + + 'linked to the framework and its associated evidence document types ' + + '(e.g. penetration_test, rbac_matrix). Document types are managed via ' + + 'the "control update --document-types" or "control add-document-type" commands.', + ) + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest(`/framework/${id}/documents`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('link-control') + .argument('', 'Framework ID') + .argument('', 'Control template ID to link') + .description( + 'Link an existing control template to this framework. ' + + 'WARNING: This auto-links the control to ALL requirements in the framework. ' + + 'For precise linking, use "control link-requirement" instead.', + ) + .action(async (frameworkId: string, controlId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/framework/${frameworkId}/link-control/${controlId}`, { + method: 'POST', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Control ${controlId} linked to framework ${frameworkId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('link-task') + .argument('', 'Framework ID') + .argument('', 'Task template ID to link') + .description( + 'Link an existing task template to this framework. ' + + 'WARNING: This auto-links the task to ALL controls in the framework. ' + + 'For precise linking, use "control link-task" instead.', + ) + .action(async (frameworkId: string, taskId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/framework/${frameworkId}/link-task/${taskId}`, { + method: 'POST', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Task ${taskId} linked to framework ${frameworkId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); + + fw.command('link-policy') + .argument('', 'Framework ID') + .argument('', 'Policy template ID to link') + .description( + 'Link an existing policy template to this framework. ' + + 'WARNING: This auto-links the policy to ALL controls in the framework. ' + + 'For precise linking, use "control link-policy" instead.', + ) + .action(async (frameworkId: string, policyId: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/framework/${frameworkId}/link-policy/${policyId}`, { + method: 'POST', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Policy ${policyId} linked to framework ${frameworkId}.`, { json }); + } catch (error) { + handleError(error, json); + } + }); +} diff --git a/packages/framework-editor-cli/src/commands/policy.ts b/packages/framework-editor-cli/src/commands/policy.ts new file mode 100644 index 0000000000..8fb05f0267 --- /dev/null +++ b/packages/framework-editor-cli/src/commands/policy.ts @@ -0,0 +1,196 @@ +import { Command } from 'commander'; +import { readFileSync } from 'node:fs'; +import { apiRequest } from '../lib/api-client.js'; +import { handleError, CliError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; +import type { PolicyTemplate } from '../types.js'; +import { FREQUENCY_VALUES, DEPARTMENT_VALUES } from '../types.js'; + +export function registerPolicyCommands(parent: Command): void { + const pol = parent + .command('policy') + .alias('pol') + .description( + 'Manage policy templates. Policies define organizational rules and procedures ' + + '(e.g. "Access Control Policy") that are linked to control templates. ' + + 'Each policy has a frequency, department, and rich-text content body.', + ); + + pol + .command('list') + .description('List policy templates. Optionally filter by framework.') + .option('--framework-id ', 'Filter policies by framework ID') + .option('--take ', 'Maximum number of results (default: 100)', '100') + .option('--skip ', 'Number of results to skip (default: 0)', '0') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/policy-template', { + query: { + frameworkId: opts.frameworkId, + take: opts.take, + skip: opts.skip, + }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + pol + .command('get') + .argument('', 'Policy template ID') + .description( + 'Get a single policy template by ID. Returns full details including content body ' + + 'and linked control templates.', + ) + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest(`/policy-template/${id}`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + pol + .command('create') + .description( + 'Create a new policy template. If --framework-id is provided, the policy ' + + 'is automatically linked to that framework.', + ) + .requiredOption('--name ', 'Policy name (e.g. "Information Security Policy")') + .requiredOption('--description ', 'Policy description') + .requiredOption( + '--frequency ', + `Review frequency (choices: ${FREQUENCY_VALUES.join(', ')})`, + ) + .requiredOption( + '--department ', + `Responsible department (choices: ${DEPARTMENT_VALUES.join(', ')})`, + ) + .option('--framework-id ', 'Framework ID to auto-link the policy to') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + validateEnum('frequency', opts.frequency, FREQUENCY_VALUES); + validateEnum('department', opts.department, DEPARTMENT_VALUES); + const data = await apiRequest('/policy-template', { + method: 'POST', + body: { + name: opts.name, + description: opts.description, + frequency: opts.frequency, + department: opts.department, + }, + query: { frameworkId: opts.frameworkId }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + pol + .command('update') + .argument('', 'Policy template ID to update') + .description('Update a policy template. Only provided fields are changed.') + .option('--name ', 'New policy name') + .option('--description ', 'New description') + .option( + '--frequency ', + `Review frequency (choices: ${FREQUENCY_VALUES.join(', ')})`, + ) + .option( + '--department ', + `Responsible department (choices: ${DEPARTMENT_VALUES.join(', ')})`, + ) + .action(async (id: string, opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const body: Record = {}; + if (opts.name !== undefined) body.name = opts.name; + if (opts.description !== undefined) body.description = opts.description; + if (opts.frequency !== undefined) { + validateEnum('frequency', opts.frequency, FREQUENCY_VALUES); + body.frequency = opts.frequency; + } + if (opts.department !== undefined) { + validateEnum('department', opts.department, DEPARTMENT_VALUES); + body.department = opts.department; + } + const data = await apiRequest(`/policy-template/${id}`, { + method: 'PATCH', + body, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + pol + .command('update-content') + .argument('', 'Policy template ID') + .description( + 'Update the rich-text content body of a policy template. ' + + 'Content is TipTap-compatible JSON. Provide via --file (path to .json file) ' + + 'or --content (inline JSON string). Max size: ~500KB.', + ) + .option('--file ', 'Path to a JSON file containing the policy content') + .option('--content ', 'Inline JSON string for the policy content') + .action(async (id: string, opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + let content: Record; + if (opts.file) { + const raw = readFileSync(opts.file, 'utf-8'); + content = JSON.parse(raw) as Record; + } else if (opts.content) { + content = JSON.parse(opts.content) as Record; + } else { + throw new CliError('Provide either --file or --content .'); + } + const data = await apiRequest(`/policy-template/${id}/content`, { + method: 'PATCH', + body: { content }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + pol + .command('delete') + .argument('', 'Policy template ID to delete') + .description('Delete a policy template permanently. This cannot be undone.') + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/policy-template/${id}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Policy template ${id} deleted.`, { json }); + } catch (error) { + handleError(error, json); + } + }); +} + +function validateEnum(field: string, value: string, allowed: readonly string[]): void { + if (!allowed.includes(value)) { + throw new CliError( + `Invalid ${field}: "${value}". Must be one of: ${allowed.join(', ')}`, + ); + } +} diff --git a/packages/framework-editor-cli/src/commands/requirement.ts b/packages/framework-editor-cli/src/commands/requirement.ts new file mode 100644 index 0000000000..5607d7b2f7 --- /dev/null +++ b/packages/framework-editor-cli/src/commands/requirement.ts @@ -0,0 +1,110 @@ +import { Command } from 'commander'; +import { apiRequest } from '../lib/api-client.js'; +import { handleError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; +import type { Requirement } from '../types.js'; + +export function registerRequirementCommands(parent: Command): void { + const req = parent + .command('requirement') + .alias('req') + .description( + 'Manage framework requirements. Requirements are specific compliance obligations ' + + '(e.g. "CC1.1") within a framework that map to control templates.', + ); + + req + .command('list') + .description( + 'List all requirements across all frameworks. Each requirement shows its framework ' + + 'association, identifier, name, and description.', + ) + .option('--take ', 'Maximum number of results to return (default: 100)', '100') + .option('--skip ', 'Number of results to skip for pagination (default: 0)', '0') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/requirement', { + query: { take: opts.take, skip: opts.skip }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + req + .command('create') + .description( + 'Create a new requirement within a framework. The requirement will be linked to ' + + 'the specified framework. Use --identifier for the standard reference code (e.g. "CC1.1").', + ) + .requiredOption('--framework-id ', 'ID of the framework this requirement belongs to') + .requiredOption('--name ', 'Requirement name (e.g. "Control Environment")') + .requiredOption('--description ', 'Detailed description of the requirement') + .option( + '--identifier ', + 'Standard reference code (e.g. "CC1.1", "A.5.1"). Optional but recommended.', + ) + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/requirement', { + method: 'POST', + body: { + frameworkId: opts.frameworkId, + name: opts.name, + description: opts.description, + identifier: opts.identifier, + }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + req + .command('update') + .argument('', 'Requirement ID to update') + .description('Update a requirement. Only provided fields are changed.') + .option('--name ', 'New requirement name') + .option('--identifier ', 'New standard reference code') + .option('--description ', 'New description') + .action(async (id: string, opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const body: Record = {}; + if (opts.name !== undefined) body.name = opts.name; + if (opts.identifier !== undefined) body.identifier = opts.identifier; + if (opts.description !== undefined) body.description = opts.description; + const data = await apiRequest(`/requirement/${id}`, { + method: 'PATCH', + body, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + req + .command('delete') + .argument('', 'Requirement ID to delete') + .description('Delete a requirement permanently. This cannot be undone.') + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/requirement/${id}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Requirement ${id} deleted.`, { json }); + } catch (error) { + handleError(error, json); + } + }); +} diff --git a/packages/framework-editor-cli/src/commands/task.ts b/packages/framework-editor-cli/src/commands/task.ts new file mode 100644 index 0000000000..1f3632c321 --- /dev/null +++ b/packages/framework-editor-cli/src/commands/task.ts @@ -0,0 +1,174 @@ +import { Command } from 'commander'; +import { apiRequest } from '../lib/api-client.js'; +import { handleError, CliError } from '../lib/errors.js'; +import { outputResult, outputSuccess } from '../lib/output.js'; +import type { TaskTemplate } from '../types.js'; +import { FREQUENCY_VALUES, DEPARTMENT_VALUES, AUTOMATION_STATUS_VALUES } from '../types.js'; + +export function registerTaskCommands(parent: Command): void { + const task = parent + .command('task') + .description( + 'Manage task templates. Tasks represent recurring compliance activities ' + + '(e.g. "Review access logs quarterly") linked to control templates. ' + + 'Each task has an optional frequency, department, and automation status.', + ); + + task + .command('list') + .description('List task templates. Optionally filter by framework.') + .option('--framework-id ', 'Filter tasks by framework ID') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest('/task-template', { + query: { frameworkId: opts.frameworkId }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + task + .command('get') + .argument('', 'Task template ID') + .description( + 'Get a single task template by ID. Returns full details including ' + + 'linked control templates.', + ) + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const data = await apiRequest(`/task-template/${id}`, { + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + task + .command('create') + .description( + 'Create a new task template. If --framework-id is provided, the task ' + + 'is automatically linked to that framework.', + ) + .requiredOption('--name ', 'Task name (e.g. "Review access control logs")') + .option('--description ', 'Task description') + .option( + '--frequency ', + `Task frequency (choices: ${FREQUENCY_VALUES.join(', ')})`, + ) + .option( + '--department ', + `Responsible department (choices: ${DEPARTMENT_VALUES.join(', ')})`, + ) + .option( + '--automation-status ', + `Automation status (choices: ${AUTOMATION_STATUS_VALUES.join(', ')}). Applied via update after creation.`, + ) + .option('--framework-id ', 'Framework ID to auto-link the task to') + .action(async (opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + if (opts.frequency) validateEnum('frequency', opts.frequency, FREQUENCY_VALUES); + if (opts.department) validateEnum('department', opts.department, DEPARTMENT_VALUES); + if (opts.automationStatus) validateEnum('automation-status', opts.automationStatus, AUTOMATION_STATUS_VALUES); + const body: Record = { name: opts.name }; + if (opts.description !== undefined) body.description = opts.description; + if (opts.frequency !== undefined) body.frequency = opts.frequency; + if (opts.department !== undefined) body.department = opts.department; + let data = await apiRequest('/task-template', { + method: 'POST', + body, + query: { frameworkId: opts.frameworkId }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + if (opts.automationStatus) { + data = await apiRequest(`/task-template/${data.id}`, { + method: 'PATCH', + body: { automationStatus: opts.automationStatus }, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + } + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + task + .command('update') + .argument('', 'Task template ID to update') + .description('Update a task template. Only provided fields are changed.') + .option('--name ', 'New task name') + .option('--description ', 'New description') + .option( + '--frequency ', + `Task frequency (choices: ${FREQUENCY_VALUES.join(', ')})`, + ) + .option( + '--department ', + `Responsible department (choices: ${DEPARTMENT_VALUES.join(', ')})`, + ) + .option( + '--automation-status ', + `Automation status (choices: ${AUTOMATION_STATUS_VALUES.join(', ')})`, + ) + .action(async (id: string, opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + const body: Record = {}; + if (opts.name !== undefined) body.name = opts.name; + if (opts.description !== undefined) body.description = opts.description; + if (opts.frequency !== undefined) { + validateEnum('frequency', opts.frequency, FREQUENCY_VALUES); + body.frequency = opts.frequency; + } + if (opts.department !== undefined) { + validateEnum('department', opts.department, DEPARTMENT_VALUES); + body.department = opts.department; + } + if (opts.automationStatus !== undefined) { + validateEnum('automation-status', opts.automationStatus, AUTOMATION_STATUS_VALUES); + body.automationStatus = opts.automationStatus; + } + const data = await apiRequest(`/task-template/${id}`, { + method: 'PATCH', + body, + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputResult(data, { json }); + } catch (error) { + handleError(error, json); + } + }); + + task + .command('delete') + .argument('', 'Task template ID to delete') + .description('Delete a task template permanently. This cannot be undone.') + .action(async (id: string, _opts, cmd) => { + const json = cmd.optsWithGlobals().json as boolean; + try { + await apiRequest(`/task-template/${id}`, { + method: 'DELETE', + apiUrl: cmd.optsWithGlobals().apiUrl, + }); + outputSuccess(`Task template ${id} deleted.`, { json }); + } catch (error) { + handleError(error, json); + } + }); +} + +function validateEnum(field: string, value: string, allowed: readonly string[]): void { + if (!allowed.includes(value)) { + throw new CliError( + `Invalid ${field}: "${value}". Must be one of: ${allowed.join(', ')}`, + ); + } +} diff --git a/packages/framework-editor-cli/src/index.ts b/packages/framework-editor-cli/src/index.ts new file mode 100644 index 0000000000..e4bd24e9af --- /dev/null +++ b/packages/framework-editor-cli/src/index.ts @@ -0,0 +1,57 @@ +import { Command } from 'commander'; +import { registerAuthCommands } from './commands/auth.js'; +import { registerFrameworkCommands } from './commands/framework.js'; +import { registerRequirementCommands } from './commands/requirement.js'; +import { registerControlCommands } from './commands/control.js'; +import { registerPolicyCommands } from './commands/policy.js'; +import { registerTaskCommands } from './commands/task.js'; + +const program = new Command(); + +program + .name('comp-framework') + .version('1.0.0', '-V') + .description( + `Comp AI Framework Editor CLI + +Manage compliance framework templates via the Comp AI API. This tool lets you +create and configure frameworks, requirements, controls, policies, and tasks +from the command line. + +ENTITY HIERARCHY: + Framework → contains Requirements + Requirement ↔ linked to Control Templates (many-to-many) + Control Template ↔ linked to Policy Templates (many-to-many) + Control Template ↔ linked to Task Templates (many-to-many) + +GETTING STARTED: + 1. Authenticate: comp-framework auth login + 2. List frameworks: comp-framework framework list + 3. Create one: comp-framework framework create --name "SOC 2" --version "2024" --description "..." + +AUTHENTICATION: + The CLI authenticates via browser-based login (same flow as the device agent). + Credentials are stored in an encrypted local config file. + Set COMP_SESSION_TOKEN env var to skip interactive login (for CI/automation). + Set COMP_API_URL env var to change the default API URL. + +OUTPUT: + All output is JSON by default for easy parsing by scripts and AI agents. + Success: { "success": true, "data": ... } + Error: { "success": false, "error": "..." } + Use --no-json for human-readable tables (e.g. comp-framework --no-json framework list).`, + ) + .option('--no-json', 'Output as human-readable tables instead of JSON') + .option( + '--api-url ', + 'API base URL (default: COMP_API_URL env var or http://localhost:3333)', + ); + +registerAuthCommands(program); +registerFrameworkCommands(program); +registerRequirementCommands(program); +registerControlCommands(program); +registerPolicyCommands(program); +registerTaskCommands(program); + +program.parse(); diff --git a/packages/framework-editor-cli/src/lib/api-client.ts b/packages/framework-editor-cli/src/lib/api-client.ts new file mode 100644 index 0000000000..1c7d573c54 --- /dev/null +++ b/packages/framework-editor-cli/src/lib/api-client.ts @@ -0,0 +1,111 @@ +import { getSessionToken, getApiUrl, clearCredentials } from './config.js'; +import { ApiError, AuthRequiredError } from './errors.js'; + +const FRAMEWORK_EDITOR_PREFIX = '/v1/framework-editor'; + +interface RequestOptions { + method?: string; + body?: Record; + query?: Record; + apiUrl?: string; + requireAuth?: boolean; +} + +function buildUrl(path: string, apiUrl: string, query?: Record): string { + const base = `${apiUrl}${FRAMEWORK_EDITOR_PREFIX}${path}`; + const params = new URLSearchParams(); + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) params.set(key, String(value)); + } + } + const qs = params.toString(); + return qs ? `${base}?${qs}` : base; +} + +export async function apiRequest(path: string, options: RequestOptions = {}): Promise { + const { method = 'GET', body, query, apiUrl: apiUrlOverride, requireAuth = true } = options; + const apiUrl = getApiUrl(apiUrlOverride); + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (requireAuth) { + const token = getSessionToken(); + if (!token) throw new AuthRequiredError(); + headers['Authorization'] = `Bearer ${token}`; + } + + const url = buildUrl(path, apiUrl, query); + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (response.status === 401) { + clearCredentials(); + throw new ApiError(401, 'Session expired. Run `comp-framework auth login` to re-authenticate.'); + } + + if (response.status === 403) { + throw new ApiError(403, 'Access denied. Platform admin privileges are required.'); + } + + if (!response.ok) { + let message: string; + try { + const errorBody = (await response.json()) as { message?: string }; + message = errorBody.message ?? response.statusText; + } catch { + message = response.statusText; + } + throw new ApiError(response.status, message); + } + + if (response.status === 204) return undefined as T; + + return (await response.json()) as T; +} + +/** + * Make a raw request to the API (not under /v1/framework-editor prefix). + * Used for auth endpoints like /api/auth/get-session. + */ +export async function rawApiRequest(path: string, options: RequestOptions = {}): Promise { + const { method = 'GET', body, apiUrl: apiUrlOverride, requireAuth = true } = options; + const apiUrl = getApiUrl(apiUrlOverride); + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (requireAuth) { + const token = getSessionToken(); + if (!token) throw new AuthRequiredError(); + headers['Authorization'] = `Bearer ${token}`; + } + + const url = `${apiUrl}${path}`; + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + let message: string; + try { + const errorBody = (await response.json()) as { message?: string }; + message = errorBody.message ?? response.statusText; + } catch { + message = response.statusText; + } + throw new ApiError(response.status, message); + } + + return (await response.json()) as T; +} diff --git a/packages/framework-editor-cli/src/lib/config.ts b/packages/framework-editor-cli/src/lib/config.ts new file mode 100644 index 0000000000..5817a95cb3 --- /dev/null +++ b/packages/framework-editor-cli/src/lib/config.ts @@ -0,0 +1,55 @@ +import Conf from 'conf'; +import type { StoredCredentials } from '../types.js'; + +const DEFAULT_API_URL = 'http://localhost:3333'; + +const store = new Conf({ + projectName: 'comp-framework-cli', + encryptionKey: 'comp-framework-cli-v1', + defaults: { + sessionToken: '', + userId: '', + apiUrl: DEFAULT_API_URL, + }, +}); + +export function getSessionToken(): string | null { + const envToken = process.env['COMP_SESSION_TOKEN']; + if (envToken) return envToken; + + const stored = store.get('sessionToken'); + return stored || null; +} + +export function getApiUrl(override?: string): string { + if (override) return override; + return process.env['COMP_API_URL'] ?? store.get('apiUrl') ?? DEFAULT_API_URL; +} + +export function getPortalUrl(apiUrl: string): string { + if (process.env['COMP_PORTAL_URL']) return process.env['COMP_PORTAL_URL']; + if (apiUrl.includes('localhost') || apiUrl.includes('127.0.0.1')) { + return 'http://localhost:3002'; + } + return apiUrl.replace('api.', 'portal.'); +} + +export function saveCredentials(sessionToken: string, userId: string, apiUrl: string): void { + store.set('sessionToken', sessionToken); + store.set('userId', userId); + store.set('apiUrl', apiUrl); +} + +export function clearCredentials(): void { + store.clear(); +} + +export function getStoredCredentials(): StoredCredentials | null { + const token = store.get('sessionToken'); + if (!token) return null; + return { + sessionToken: token, + userId: store.get('userId'), + apiUrl: store.get('apiUrl'), + }; +} diff --git a/packages/framework-editor-cli/src/lib/errors.ts b/packages/framework-editor-cli/src/lib/errors.ts new file mode 100644 index 0000000000..4fc9afc198 --- /dev/null +++ b/packages/framework-editor-cli/src/lib/errors.ts @@ -0,0 +1,43 @@ +export class CliError extends Error { + constructor( + message: string, + public exitCode: number = 1, + ) { + super(message); + this.name = 'CliError'; + } +} + +export class AuthRequiredError extends CliError { + constructor() { + super('Not authenticated. Run `comp-framework auth login` first.'); + this.name = 'AuthRequiredError'; + } +} + +export class ApiError extends CliError { + constructor( + public status: number, + message: string, + ) { + super(`API error (${status}): ${message}`); + this.name = 'ApiError'; + } +} + +export function handleError(error: unknown, json: boolean): never { + const message = + error instanceof CliError + ? error.message + : error instanceof Error + ? error.message + : String(error); + const exitCode = error instanceof CliError ? error.exitCode : 1; + + if (json) { + console.log(JSON.stringify({ success: false, error: message })); + } else { + console.error(`Error: ${message}`); + } + process.exit(exitCode); +} diff --git a/packages/framework-editor-cli/src/lib/output.ts b/packages/framework-editor-cli/src/lib/output.ts new file mode 100644 index 0000000000..f45e0afadc --- /dev/null +++ b/packages/framework-editor-cli/src/lib/output.ts @@ -0,0 +1,100 @@ +interface OutputOptions { + json: boolean; +} + +export function outputResult(data: T, opts: OutputOptions): void { + if (opts.json) { + console.log(JSON.stringify({ success: true, data }, null, 2)); + return; + } + if (data === undefined || data === null) { + console.log('Done.'); + return; + } + if (Array.isArray(data)) { + outputTable(data); + return; + } + if (typeof data === 'object') { + outputRecord(data as Record); + return; + } + console.log(String(data)); +} + +export function outputSuccess(message: string, opts: OutputOptions): void { + if (opts.json) { + console.log(JSON.stringify({ success: true, message })); + } else { + console.log(message); + } +} + +function outputRecord(record: Record): void { + const maxKeyLen = Math.max(...Object.keys(record).map((k) => k.length)); + for (const [key, value] of Object.entries(record)) { + const displayValue = formatValue(value); + console.log(` ${key.padEnd(maxKeyLen)} ${displayValue}`); + } +} + +function outputTable(rows: unknown[]): void { + if (rows.length === 0) { + console.log('No results found.'); + return; + } + + const first = rows[0] as Record; + const keys = Object.keys(first).filter((k) => !isComplexField(first[k])); + + const widths: Record = {}; + for (const key of keys) { + widths[key] = key.length; + } + for (const row of rows) { + const r = row as Record; + for (const key of keys) { + const val = truncate(formatValue(r[key]), 50); + widths[key] = Math.max(widths[key] ?? 0, val.length); + } + } + + const header = keys.map((k) => k.padEnd(widths[k] ?? 0)).join(' '); + const separator = keys.map((k) => '-'.repeat(widths[k] ?? 0)).join(' '); + + console.log(header); + console.log(separator); + + for (const row of rows) { + const r = row as Record; + const line = keys.map((k) => truncate(formatValue(r[k]), 50).padEnd(widths[k] ?? 0)).join(' '); + console.log(line); + } + + console.log(`\n${rows.length} result(s)`); +} + +function formatValue(value: unknown): string { + if (value === null || value === undefined) return '-'; + if (typeof value === 'boolean') return value ? 'yes' : 'no'; + if (Array.isArray(value)) return value.length > 0 ? value.map(formatArrayItem).join(', ') : '-'; + if (typeof value === 'object') return JSON.stringify(value); + return String(value); +} + +function formatArrayItem(item: unknown): string { + if (typeof item === 'object' && item !== null && 'name' in item) { + return String((item as { name: string }).name); + } + return String(item); +} + +function truncate(str: string, max: number): string { + return str.length > max ? str.slice(0, max - 1) + '…' : str; +} + +function isComplexField(value: unknown): boolean { + if (Array.isArray(value)) return true; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) return true; + return false; +} diff --git a/packages/framework-editor-cli/src/types.ts b/packages/framework-editor-cli/src/types.ts new file mode 100644 index 0000000000..64c01c02ff --- /dev/null +++ b/packages/framework-editor-cli/src/types.ts @@ -0,0 +1,115 @@ +export interface StoredCredentials { + sessionToken: string; + userId: string; + apiUrl: string; +} + +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +export interface Framework { + id: string; + name: string; + version: string; + description: string; + visible: boolean; + createdAt: string; + updatedAt: string; +} + +export interface FrameworkWithCounts extends Framework { + requirementsCount?: number; + controlsCount?: number; +} + +export interface Requirement { + id: string; + frameworkId: string; + name: string; + identifier: string | null; + description: string; + createdAt: string; + updatedAt: string; + controlTemplates?: Array<{ id: string; name: string }>; +} + +export interface ControlTemplate { + id: string; + name: string; + description: string; + documentTypes: string[]; + createdAt: string; + updatedAt: string; + policyTemplates?: Array<{ id: string; name: string }>; + requirements?: Array<{ id: string; name: string }>; + taskTemplates?: Array<{ id: string; name: string }>; +} + +export interface PolicyTemplate { + id: string; + name: string; + description: string; + frequency: Frequency; + department: Department; + content: Record | null; + createdAt: string; + updatedAt: string; + controlTemplates?: Array<{ id: string; name: string }>; +} + +export interface TaskTemplate { + id: string; + name: string; + description: string | null; + frequency: Frequency | null; + department: Department | null; + automationStatus: TaskAutomationStatus | null; + createdAt: string; + updatedAt: string; + controlTemplates?: Array<{ id: string; name: string }>; +} + +export interface ControlDocument { + id: string; + name: string; + documentTypes: string[]; +} + +export type Frequency = 'monthly' | 'quarterly' | 'yearly'; +export type Department = 'none' | 'admin' | 'gov' | 'hr' | 'it' | 'itsm' | 'qms'; +export type TaskAutomationStatus = 'AUTOMATED' | 'MANUAL'; + +export type EvidenceFormType = + | 'board_meeting' + | 'it_leadership_meeting' + | 'risk_committee_meeting' + | 'meeting' + | 'access_request' + | 'whistleblower_report' + | 'penetration_test' + | 'rbac_matrix' + | 'infrastructure_inventory' + | 'employee_performance_evaluation' + | 'network_diagram' + | 'tabletop_exercise'; + +export const FREQUENCY_VALUES: Frequency[] = ['monthly', 'quarterly', 'yearly']; +export const DEPARTMENT_VALUES: Department[] = ['none', 'admin', 'gov', 'hr', 'it', 'itsm', 'qms']; +export const AUTOMATION_STATUS_VALUES: TaskAutomationStatus[] = ['AUTOMATED', 'MANUAL']; +export const EVIDENCE_FORM_TYPE_VALUES: EvidenceFormType[] = [ + 'board_meeting', + 'it_leadership_meeting', + 'risk_committee_meeting', + 'meeting', + 'access_request', + 'whistleblower_report', + 'penetration_test', + 'rbac_matrix', + 'infrastructure_inventory', + 'employee_performance_evaluation', + 'network_diagram', + 'tabletop_exercise', +]; diff --git a/packages/framework-editor-cli/tsconfig.json b/packages/framework-editor-cli/tsconfig.json new file mode 100644 index 0000000000..f0ef2381b3 --- /dev/null +++ b/packages/framework-editor-cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + } +} diff --git a/packages/framework-editor-cli/tsup.config.ts b/packages/framework-editor-cli/tsup.config.ts new file mode 100644 index 0000000000..d1f014ee16 --- /dev/null +++ b/packages/framework-editor-cli/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + target: 'node18', + clean: true, + dts: false, + banner: { + js: '#!/usr/bin/env node', + }, +}); diff --git a/packages/ui/src/components/editor/utils/validate-content.ts b/packages/ui/src/components/editor/utils/validate-content.ts index ae71c5b343..8626e3b372 100644 --- a/packages/ui/src/components/editor/utils/validate-content.ts +++ b/packages/ui/src/components/editor/utils/validate-content.ts @@ -9,25 +9,28 @@ export function validateAndFixTipTapContent(content: any): JSONContent { return createEmptyDocument(); } + // Unwrap corrupted { set: [...] } wrapper (from Prisma createMany bug) + const unwrapped = unwrapSetWrapper(content); + // If it's already a proper doc, validate its content - if (content.type === 'doc' && content.content) { + if (unwrapped.type === 'doc' && unwrapped.content) { return { type: 'doc', - content: fixContentArray(content.content), + content: fixContentArray(unwrapped.content), }; } // If it's an array, wrap it in a doc - if (Array.isArray(content)) { + if (Array.isArray(unwrapped)) { return { type: 'doc', - content: fixContentArray(content), + content: fixContentArray(unwrapped), }; } // If it's a single node, wrap it in a doc - if (content.type && content.type !== 'doc') { - const fixedNode = fixNode(content); + if (unwrapped.type && unwrapped.type !== 'doc') { + const fixedNode = fixNode(unwrapped); return { type: 'doc', content: fixedNode ? [fixedNode] : [createEmptyParagraph()], @@ -38,6 +41,35 @@ export function validateAndFixTipTapContent(content: any): JSONContent { return createEmptyDocument(); } +/** + * Unwraps a `{ set: [...] }` wrapper that Prisma's createMany incorrectly + * stored for Json[] fields. If the value is an array containing a single + * `{ set: [...] }` element, returns the inner array. Also handles the case + * where the top-level value itself is `{ set: [...] }`. + */ +function unwrapSetWrapper(value: any): any { + if (!value || typeof value !== 'object') return value; + + // Direct { set: [...] } object + if (!Array.isArray(value) && Array.isArray(value.set)) { + return value.set; + } + + // Array containing a single { set: [...] } element + if ( + Array.isArray(value) && + value.length === 1 && + value[0] && + typeof value[0] === 'object' && + !Array.isArray(value[0]) && + Array.isArray(value[0].set) + ) { + return value[0].set; + } + + return value; +} + /** * Tries to parse a stringified JSON node into a proper object. * Returns the parsed object or null if parsing fails. @@ -66,12 +98,20 @@ function fixContentArray(contentArray: any[]): JSONContent[] { // First pass: parse any stringified JSON nodes const parsed = contentArray.map((node) => tryParseStringNode(node) ?? node); - // Second pass: fix each node - const fixedNodes = parsed + // Second pass: flatten any nested doc nodes (content stored as [{type:"doc",content:[...]}]) + const flattened = parsed.flatMap((node) => { + if (node && typeof node === 'object' && node.type === 'doc' && Array.isArray(node.content)) { + return node.content; + } + return [node]; + }); + + // Third pass: fix each node + const fixedNodes = flattened .map(fixNode) .filter((node): node is JSONContent => node !== null) as JSONContent[]; - // Third pass: merge orphaned listItems into bulletLists + // Fourth pass: merge orphaned listItems into bulletLists const merged = mergeOrphanedListItems(fixedNodes); // Ensure we have at least one paragraph From 59e0db91ae640ae21d340666fb6e23e169fb8f12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:19:10 -0400 Subject: [PATCH 13/31] feat: migrate prisma from v6 to v7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(db): update prisma schema for v7 (provider, output, remove url) * chore(db): rewrite prisma.config.ts for v7 stable API * chore(db): update client/index to use generated prisma-client path * chore(db): bump prisma to v7, add adapter-pg, update scripts and dist templates * chore(db): update postinstall sentinel for prisma-client output path * chore(db): add previewFeatures postgresqlExtensions to generator for prisma v7 compat * chore(auth): replace PrismaClient import with PrismaLike structural type for prisma v7 - Remove @prisma/client import and peerDependency - Replace PrismaClient type with PrismaLike interface (duck typing) - Make CreateAuthServerOptions generic with TDb extends PrismaLike - Define PrismaLike with $connect, $disconnect, and organization model methods - Removes direct coupling to @prisma/client for Prisma v7 custom output compatibility Co-Authored-By: Claude Sonnet 4.6 * fix(auth): simplify PrismaLike to minimal structural interface * chore(api): add patch-schema-generator script for prisma-client-js in v7 * chore(api): unignore patch-schema-generator.js from scripts gitignore * chore(api): update prisma config, client, index for v7 + add adapter-pg Co-Authored-By: Claude Sonnet 4.6 * fix: prisma PrismaPg constructor requires PoolConfig object, not raw string * chore(api): fix patch-schema to include postgresqlExtensions previewFeature and add prisma/generated to gitignore Co-Authored-By: Claude Sonnet 4.6 * chore(api): migrate @trycompai/db imports to @db * chore(api): migrate @prisma/client imports to @db * fix(api): use default prisma-client-js output to resolve dist/ module issue prisma-client-js without custom output generates to node_modules/@prisma/client, which Node.js resolves from any directory. This avoids the ./generated import that fails at runtime because tsc doesn't copy .js files to dist/. Co-Authored-By: Claude Opus 4.6 (1M context) * chore(app): update prisma to v7 (schema, config, client, remove monorepo plugin) Co-Authored-By: Claude Sonnet 4.6 * chore(app): migrate @trycompai/db and @prisma/client imports to @db Co-Authored-By: Claude Opus 4.6 (1M context) * chore(portal): update prisma to v7 (schema, config, client, imports) - Switch generator from prisma-client-js to prisma-client with output to src/generated/prisma - Add prisma.config.ts with defineConfig + env datasource URL - Update client.ts to use PrismaPg adapter with PoolConfig pattern - Update index.ts to re-export from generated client - Add @prisma/adapter-pg 7.6.0 and bump @prisma/client + prisma to 7.6.0 - Add src/generated/ to .gitignore Co-Authored-By: Claude Sonnet 4.6 * chore(api): update Dockerfile for prisma v7 (patch step, remove openssl) Co-Authored-By: Claude Sonnet 4.6 * chore(ci): add schema patch step to trigger.dev deploy workflows for prisma v7 Co-Authored-By: Claude Sonnet 4.6 * chore: bump @prisma/adapter-pg to 7.6.0 at root Co-Authored-By: Claude Sonnet 4.6 * chore(framework-editor): bump prisma to v7, add prisma.config.ts * fix(db): update seed script to use PrismaPg adapter instead of removed datasourceUrl * fix(app): split prisma exports into browser-safe types and server-only db prisma-client v7's PrismaPg adapter imports pg which requires dns (Node.js only). Client components importing types from @db would pull in the entire server chain. Fix: @db now exports from browser.ts (types/enums only), @db/server exports the db instance and full client types. 102 server files migrated to @db/server. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(app): migrate remaining multi-line db import to @db/server * fix(db): dist templates use ../src/generated path and browser.ts for types * fix(db): remove db export from dist/index and use browser.ts for all barrel exports * fix(db): point exports to src/index.ts, use browser.ts for barrel export * fix(app): add resolveExtensions to turbopack for .js→.ts resolution in generated prisma files * fix: add post-generate script to rewrite .js→.ts imports in prisma generated files * fix(db): remove db export from index.ts — server-only, must be imported from client.ts directly * fix(db): add .ts extension to browser import for Node.js ESM resolution * fix(db): keep .js extensions in packages/db for tsc compat, only fix for Turbopack apps * fix(db): use combined schema (dist/schema.prisma) for prisma generate to include all models and enums * fix(app): migrate remaining db and Prisma imports to @db/server * perf(db): re-export from @prisma/client, skip compiling generated files in build @prisma/client has browser-safe exports with all enums and proper browser/server conditions. No need to compile hundreds of generated .ts files to dist/ — build goes from 6+ min in Docker to ~3s. * perf(db): fast build — generate prisma-client-js to @prisma/client, skip tsc of generated files * fix: add @trycompai/db to db:generate filter, use generate-prisma-client-js in db:generate * fix(app): update trigger.dev config and custom prisma extension for v7 * fix(api): update trigger.dev config and custom prisma extension for v7 * fix: change @trycompai/db from npm pinned version to workspace:* across all packages * fix(portal): split prisma exports into browser-safe types and server-only db * fix(framework-editor): update prisma client for v7, add adapter-pg, split browser/server exports * fix(portal): use session cookies instead of JWT for API calls (API supports cookies via HybridAuthGuard) * fix(portal): remove unnecessary X-Organization-Id header, API gets it from session * fix(api): add localhost:3008 (trust app) to CORS trusted origins * fix(api): update jest mocks from @trycompai/db to @db, add missing @db mocks for test files * chore: commit pre-existing spec and openapi changes * fix: address bugbot issues (test mocks, duplicate mocks, build:docker prisma step) - policies.controller.spec.ts: change require('@trycompai/db') to require('@db') to match the mocked module - invitations route.test.ts: change vi.mock('@db') to vi.mock('@db/server') to match the import in the test - risks/comments/devices controller specs: remove duplicate jest.mock('@db') blocks, keeping one per file - apps/app/package.json: add fix-generated-extensions.js step to build:docker script Co-Authored-By: Claude Sonnet 4.6 * fix: add db:generate task dependency to prevent race condition in Vercel builds * chore: update bun.lock * fix(db): import PrismaClient from @prisma/client in src/client.ts (matches build output) * fix(portal): migrate fleet-policies route db import to @db/server * fix: remove duplicate jest.mock, fix fragile prisma-client regex in trigger extensions Co-Authored-By: Claude Opus 4.6 (1M context) * fix: scope sed output deletion to generator directive only (prevents deleting model fields) * fix: prevent PrismaPg adapter pool leak during dev hot-reload * fix(app): import Prisma from @db/server in connect-cloud action * fix(api): migrate new @trycompai/db imports from merged PR to @db * fix: make schema patching surgical (replace provider + remove output only, preserve other settings) --------- Co-authored-by: Mariano Fuentes Co-authored-by: Claude Sonnet 4.6 --- .../trigger-api-tasks-deploy-main.yml | 1 + .../trigger-api-tasks-deploy-release.yml | 1 + apps/api/.gitignore | 3 +- apps/api/Dockerfile.multistage | 3 +- apps/api/customPrismaExtension.ts | 18 +- apps/api/package.json | 11 +- apps/api/prisma.config.ts | 9 + apps/api/prisma/client.d.ts | 2 - apps/api/prisma/client.ts | 8 +- apps/api/scripts/patch-schema-generator.js | 15 ++ .../admin-context.controller.spec.ts | 2 +- .../admin-evidence.controller.spec.ts | 2 +- .../admin-findings.controller.spec.ts | 2 +- .../admin-findings.controller.ts | 2 +- .../admin-guard-integration.spec.ts | 2 +- .../admin-organizations.controller.spec.ts | 2 +- .../admin-organizations.service.spec.ts | 4 +- .../admin-organizations.service.ts | 2 +- .../admin-policies.controller.spec.ts | 2 +- .../admin-policies.controller.ts | 2 +- .../admin-security.spec.ts | 2 +- .../admin-tasks.controller.spec.ts | 2 +- .../admin-tasks.controller.ts | 2 +- .../admin-vendors.controller.spec.ts | 2 +- .../admin-vendors.controller.ts | 2 +- .../dto/create-admin-vendor.dto.ts | 2 +- .../assistant-chat/assistant-chat-tools.ts | 2 +- .../src/audit/audit-log.controller.spec.ts | 12 +- apps/api/src/audit/audit-log.controller.ts | 2 +- apps/api/src/auth/api-key.service.ts | 2 +- apps/api/src/auth/auth.controller.ts | 2 +- apps/api/src/auth/auth.server.ts | 3 +- apps/api/src/auth/hybrid-auth.guard.ts | 2 +- .../api/src/auth/platform-admin.guard.spec.ts | 2 +- apps/api/src/auth/platform-admin.guard.ts | 2 +- apps/api/src/auth/types.ts | 2 +- .../src/browserbase/browserbase.service.ts | 2 +- .../cloud-security-legacy.service.ts | 2 +- .../src/comments/comments.controller.spec.ts | 8 +- apps/api/src/comments/comments.service.ts | 2 +- apps/api/src/context/context.service.ts | 2 +- apps/api/src/controls/controls.service.ts | 2 +- .../device-agent-auth.service.spec.ts | 8 +- .../device-agent/device-agent-auth.service.ts | 2 +- .../device-registration.helpers.ts | 2 +- .../src/devices/devices.controller.spec.ts | 22 ++- apps/api/src/devices/devices.service.ts | 2 +- .../evidence-forms.service.spec.ts | 4 +- .../evidence-forms/evidence-forms.service.ts | 2 +- .../finding-template.service.ts | 2 +- .../src/findings/dto/create-finding.dto.ts | 2 +- .../src/findings/dto/update-finding.dto.ts | 2 +- .../src/findings/findings.controller.spec.ts | 2 +- apps/api/src/findings/findings.controller.ts | 4 +- apps/api/src/findings/findings.service.ts | 2 +- .../control-template.service.ts | 4 +- .../framework/dto/import-framework.dto.ts | 2 +- .../framework/framework-export.service.ts | 2 +- .../framework/framework.service.ts | 2 +- .../dto/create-policy-template.dto.ts | 2 +- .../policy-template.service.ts | 2 +- .../requirement/requirement.service.ts | 2 +- .../dto/create-task-template.dto.ts | 2 +- .../dto/task-template-response.dto.ts | 2 +- .../dto/update-task-template.dto.ts | 2 +- .../task-template/task-template.service.ts | 2 +- .../frameworks/frameworks-scores.helper.ts | 2 +- .../frameworks/frameworks-upsert.helper.ts | 2 +- .../src/frameworks/frameworks.service.spec.ts | 4 +- apps/api/src/frameworks/frameworks.service.ts | 2 +- .../controllers/checks.controller.ts | 2 +- .../dynamic-integrations.controller.ts | 2 +- .../controllers/sync.controller.ts | 2 +- .../task-integrations.controller.ts | 2 +- .../platform-audit-log.interceptor.ts | 2 +- .../repositories/check-run.repository.ts | 2 +- .../repositories/connection.repository.ts | 2 +- .../repositories/credential.repository.ts | 2 +- .../repositories/dynamic-check.repository.ts | 2 +- .../dynamic-integration.repository.ts | 4 +- .../repositories/oauth-app.repository.ts | 2 +- .../repositories/oauth-state.repository.ts | 2 +- .../platform-credential.repository.ts | 2 +- .../repositories/provider.repository.ts | 2 +- .../services/connection.service.ts | 2 +- .../dynamic-manifest-loader.service.ts | 2 +- .../services/generic-employee-sync.service.ts | 2 +- .../integration-sync-logger.service.ts | 2 +- .../services/oauth-credentials.service.ts | 2 +- apps/api/src/org-chart/org-chart.service.ts | 2 +- .../src/organization/organization.service.ts | 2 +- apps/api/src/people/dto/create-people.dto.ts | 2 +- .../src/people/dto/people-responses.dto.ts | 2 +- apps/api/src/people/people-fleet.helper.ts | 2 +- .../src/people/people-invite.service.spec.ts | 4 +- apps/api/src/people/people-invite.service.ts | 2 +- apps/api/src/people/people.service.spec.ts | 4 +- apps/api/src/people/people.service.ts | 2 +- .../src/people/utils/member-deactivation.ts | 2 +- apps/api/src/people/utils/member-queries.ts | 2 +- apps/api/src/people/utils/member-validator.ts | 2 +- .../src/policies/policies.controller.spec.ts | 10 +- apps/api/src/policies/policies.controller.ts | 2 +- apps/api/src/policies/policies.service.ts | 2 +- apps/api/src/risks/dto/create-risk.dto.ts | 2 +- apps/api/src/risks/dto/get-risks-query.dto.ts | 2 +- apps/api/src/risks/dto/risk-response.dto.ts | 2 +- apps/api/src/risks/risks.controller.spec.ts | 44 +++-- apps/api/src/risks/risks.service.ts | 2 +- apps/api/src/roles/roles.service.spec.ts | 4 +- apps/api/src/roles/roles.service.ts | 2 +- apps/api/src/secrets/secrets.service.ts | 2 +- .../pentest-billing.service.ts | 2 +- ...security-penetration-tests.service.spec.ts | 8 +- .../security-penetration-tests.service.ts | 2 +- .../task-management.controller.ts | 2 +- apps/api/src/tasks/attachments.service.ts | 2 +- .../tasks/automations/automations.service.ts | 2 +- .../evidence-export.service.ts | 2 +- apps/api/src/tasks/tasks.controller.spec.ts | 16 +- apps/api/src/tasks/tasks.service.ts | 2 +- .../training-certificate-pdf.service.spec.ts | 2 +- .../trigger/policies/update-policy-helpers.ts | 4 +- .../trigger/policies/update-policy-prompts.ts | 2 +- .../vendor/vendor-risk-assessment-task.ts | 2 +- .../dto/compliance-resource.dto.ts | 2 +- .../trust-portal/trust-access.controller.ts | 2 +- .../src/trust-portal/trust-access.service.ts | 2 +- .../src/trust-portal/trust-portal.service.ts | 2 +- apps/api/src/utils/assignment-filter.ts | 2 +- apps/api/src/utils/compliance-filters.ts | 2 +- .../src/utils/department-visibility.spec.ts | 2 +- apps/api/src/utils/department-visibility.ts | 2 +- apps/api/src/vendors/dto/create-vendor.dto.ts | 2 +- apps/api/src/vendors/dto/update-vendor.dto.ts | 2 +- .../src/vendors/dto/vendor-response.dto.ts | 2 +- apps/api/src/vendors/vendors.service.ts | 4 +- apps/api/test/maced-contract.e2e-spec.ts | 9 +- apps/api/trigger.config.ts | 4 +- apps/app/.gitignore | 1 + apps/app/customPrismaExtension.ts | 19 +- apps/app/next.config.ts | 10 +- apps/app/package.json | 14 +- apps/app/prisma.config.ts | 9 + apps/app/prisma/client.ts | 10 +- apps/app/prisma/index.ts | 3 +- apps/app/prisma/server.ts | 2 + apps/app/src/actions/files/upload-file.ts | 2 +- .../actions/organization/accept-invitation.ts | 2 +- .../organization/get-api-keys-action.ts | 2 +- .../get-organization-users-action.ts | 2 +- .../organization/lib/get-framework-names.ts | 2 +- .../lib/initialize-organization.ts | 2 +- .../actions/organization/remove-employee.ts | 2 +- ...organization-access-request-form-action.ts | 2 +- ...e-organization-device-agent-step-action.ts | 2 +- ...e-organization-evidence-approval-action.ts | 2 +- ...anization-security-training-step-action.ts | 2 +- ...rganization-whistleblower-report-action.ts | 2 +- .../actions/people/create-employee-action.ts | 4 +- .../accept-requested-policy-changes.ts | 2 +- .../policies/update-version-content.ts | 2 +- apps/app/src/actions/safe-action.ts | 2 +- .../cloud-tests/actions/connect-cloud.ts | 3 +- .../getAllFrameworkInstancesWithControls.ts | 2 +- .../(app)/[orgId]/frameworks/lib/getPeople.ts | 2 +- .../[orgId]/frameworks/lib/getPolicies.ts | 2 +- .../(app)/[orgId]/frameworks/lib/getTasks.ts | 2 +- .../lib/taskEvidenceDocumentsScore.ts | 2 +- apps/app/src/app/(app)/[orgId]/layout.tsx | 2 +- .../[employeeId]/actions/update-employee.ts | 2 +- .../[orgId]/people/[employeeId]/page.tsx | 2 +- .../people/all/actions/checkMemberStatus.ts | 2 +- .../people/all/actions/inviteNewMember.ts | 2 +- .../people/all/actions/reactivateMember.ts | 2 +- .../people/all/actions/revokeInvitation.ts | 2 +- .../people/all/components/TeamMembers.tsx | 2 +- .../components/EmployeesOverview.tsx | 2 +- .../[orgId]/people/devices/data/index.ts | 2 +- .../app/src/app/(app)/[orgId]/people/page.tsx | 2 +- .../[policyId]/actions/delete-policy-pdf.ts | 2 +- .../[policyId]/actions/get-policy-pdf-url.ts | 2 +- .../[policyId]/actions/upload-policy-pdf.ts | 2 +- .../editor/actions/get-policy-details.ts | 2 +- .../editor/actions/update-policy.ts | 2 +- .../penetration-tests/[reportId]/page.tsx | 2 +- .../security/penetration-tests/page.tsx | 2 +- .../src/app/(app)/[orgId]/settings/page.tsx | 2 +- .../(app)/[orgId]/settings/portal/page.tsx | 2 +- .../actions/generate-suggestions.ts | 2 +- .../automation/[automationId]/page.tsx | 2 +- .../actions/check-dns-record.ts | 2 +- .../actions/update-vendor-action.ts | 2 +- .../components/VendorDetailTabs.tsx | 2 +- .../components/VendorResearchSection.tsx | 2 +- .../components/charts/vendors-by-category.tsx | 2 +- .../components/charts/vendors-by-status.tsx | 2 +- apps/app/src/app/(app)/invite/[code]/page.tsx | 2 +- .../src/app/(app)/onboarding/[orgId]/page.tsx | 2 +- .../onboarding/actions/complete-onboarding.ts | 2 +- .../actions/create-organization-minimal.ts | 2 +- .../setup/actions/create-organization.ts | 2 +- .../src/app/(app)/upgrade/[orgId]/page.tsx | 2 +- apps/app/src/app/api/auth/test-db/route.ts | 2 +- .../app/api/auth/test-grant-access/route.ts | 2 +- apps/app/src/app/api/auth/test-login/route.ts | 2 +- .../src/app/api/email-preferences/route.ts | 2 +- apps/app/src/app/api/get-image-url/route.ts | 2 +- apps/app/src/app/api/health/route.ts | 2 +- .../app/api/invitations/[id]/route.test.ts | 4 +- .../app/api/policies/[policyId]/chat/route.ts | 2 +- apps/app/src/app/api/qa/approve-org/route.ts | 2 +- apps/app/src/app/api/qa/delete-user/route.ts | 2 +- .../app/src/app/api/retool/reset-org/route.ts | 2 +- .../src/app/api/training/certificate/route.ts | 2 +- apps/app/src/app/api/user-frameworks/route.ts | 2 +- .../app/api/webhooks/stripe-pentest/route.ts | 2 +- apps/app/src/app/page.tsx | 2 +- apps/app/src/app/unsubscribe/page.tsx | 2 +- .../src/app/unsubscribe/preferences/page.tsx | 2 +- .../policies/charts/policies-by-assignee.tsx | 2 +- .../risks/charts/risks-by-status.tsx | 2 +- apps/app/src/components/sidebar.tsx | 2 +- apps/app/src/hooks/use-vendors.ts | 4 +- apps/app/src/lib/api-key.ts | 2 +- apps/app/src/lib/compliance.ts | 2 +- apps/app/src/lib/currentOrganization.ts | 2 +- apps/app/src/lib/db/employee.ts | 2 +- apps/app/src/lib/org-chart.ts | 2 +- apps/app/src/lib/permissions.server.ts | 2 +- .../src/lib/vector/sync/sync-manual-answer.ts | 2 +- .../src/lib/vector/sync/sync-organization.ts | 2 +- .../tasks/auditor/generate-auditor-content.ts | 2 +- .../device/create-fleet-label-for-all-orgs.ts | 2 +- .../device/create-fleet-label-for-org.ts | 2 +- .../trigger/tasks/email/new-policy-email.ts | 2 +- .../tasks/email/publish-all-policies-email.ts | 2 +- .../tasks/email/weekly-task-digest-email.ts | 2 +- .../tasks/integration/integration-results.ts | 2 +- .../tasks/integration/integration-schedule.ts | 2 +- .../integration/run-integration-tests.ts | 2 +- .../backfill-executive-context-all-orgs.ts | 2 +- .../backfill-executive-context-single-org.ts | 2 +- .../backfill-training-videos-for-all-orgs.ts | 2 +- .../backfill-training-videos-for-org.ts | 2 +- .../onboarding/generate-full-policies.ts | 2 +- .../onboarding/generate-risk-mitigation.ts | 2 +- .../onboarding/generate-vendor-mitigation.ts | 2 +- .../onboard-organization-helpers.ts | 2 +- .../tasks/onboarding/onboard-organization.ts | 2 +- .../onboarding/update-policies-helpers.ts | 2 +- apps/app/src/trigger/tasks/scrape/research.ts | 2 +- .../src/trigger/tasks/task/policy-schedule.ts | 2 +- .../tasks/task/task-schedule-helpers.ts | 2 +- .../src/trigger/tasks/task/task-schedule.ts | 2 +- .../tasks/task/weekly-task-reminder.ts | 2 +- apps/app/trigger.config.ts | 4 +- apps/app/tsconfig.json | 3 + apps/framework-editor/.gitignore | 1 + apps/framework-editor/package.json | 5 +- apps/framework-editor/prisma.config.ts | 9 + apps/framework-editor/prisma/client.ts | 14 +- apps/framework-editor/prisma/index.ts | 3 +- .../prisma/server.ts} | 0 apps/framework-editor/tsconfig.json | 5 +- apps/portal/.gitignore | 1 + apps/portal/package.json | 9 +- apps/portal/prisma.config.ts | 9 + apps/portal/prisma/client.ts | 10 +- apps/portal/prisma/index.ts | 3 +- apps/portal/prisma/server.ts | 2 + apps/portal/src/actions/accept-policies.ts | 2 +- .../components/OrganizationDashboard.tsx | 2 +- .../[orgId]/documents/[formType]/page.tsx | 3 +- .../documents/[formType]/submissions/page.tsx | 13 +- .../src/app/(app)/(home)/[orgId]/page.tsx | 2 +- .../(home)/[orgId]/policy/[policyId]/page.tsx | 2 +- .../app/(app)/(home)/components/Overview.tsx | 2 +- .../src/app/api/confirm-fleet-policy/route.ts | 2 +- .../src/app/api/download-agent/utils.ts | 2 +- .../src/app/api/fleet-policies/route.ts | 2 +- apps/portal/src/app/api/fleet-policy/route.ts | 2 +- .../app/api/portal/accept-policies/route.ts | 2 +- .../api/portal/mark-policy-completed/route.ts | 2 +- .../app/api/portal/policy-pdf-url/route.ts | 2 +- apps/portal/tsconfig.json | 1 + bun.lock | 186 +++++++++++++----- package.json | 4 +- packages/auth/package.json | 4 - packages/auth/src/server.ts | 75 ++++--- packages/company/package.json | 2 +- packages/db/.gitignore | 2 +- packages/db/package.json | 19 +- packages/db/prisma.config.ts | 16 +- packages/db/prisma/schema.prisma | 6 +- packages/db/prisma/seed/seed.ts | 6 +- packages/db/scripts/combine-schemas.js | 13 +- .../db/scripts/fix-generated-extensions.js | 37 ++++ .../db/scripts/generate-prisma-client-js.js | 31 +++ packages/db/src/client.ts | 9 +- packages/db/src/index.ts | 1 - packages/db/src/postinstall.ts | 2 +- packages/db/tsconfig.json | 2 +- turbo.json | 3 +- 304 files changed, 810 insertions(+), 493 deletions(-) create mode 100644 apps/api/prisma.config.ts delete mode 100644 apps/api/prisma/client.d.ts create mode 100644 apps/api/scripts/patch-schema-generator.js create mode 100644 apps/app/prisma.config.ts create mode 100644 apps/app/prisma/server.ts create mode 100644 apps/framework-editor/.gitignore create mode 100644 apps/framework-editor/prisma.config.ts rename apps/{api/prisma/index.d.ts => framework-editor/prisma/server.ts} (100%) create mode 100644 apps/portal/prisma.config.ts create mode 100644 apps/portal/prisma/server.ts create mode 100644 packages/db/scripts/fix-generated-extensions.js create mode 100644 packages/db/scripts/generate-prisma-client-js.js diff --git a/.github/workflows/trigger-api-tasks-deploy-main.yml b/.github/workflows/trigger-api-tasks-deploy-main.yml index bf6a67c963..6842f036a1 100644 --- a/.github/workflows/trigger-api-tasks-deploy-main.yml +++ b/.github/workflows/trigger-api-tasks-deploy-main.yml @@ -42,6 +42,7 @@ jobs: run: | mkdir -p prisma cp ../../packages/db/dist/schema.prisma prisma/schema.prisma + node scripts/patch-schema-generator.js bunx prisma generate - name: 🚀 Deploy Trigger.dev working-directory: ./apps/api diff --git a/.github/workflows/trigger-api-tasks-deploy-release.yml b/.github/workflows/trigger-api-tasks-deploy-release.yml index 9a3aa6eda6..fa3912797e 100644 --- a/.github/workflows/trigger-api-tasks-deploy-release.yml +++ b/.github/workflows/trigger-api-tasks-deploy-release.yml @@ -48,6 +48,7 @@ jobs: run: | mkdir -p prisma cp ../../packages/db/dist/schema.prisma prisma/schema.prisma + node scripts/patch-schema-generator.js bunx prisma generate - name: 🚀 Deploy Trigger.dev diff --git a/apps/api/.gitignore b/apps/api/.gitignore index b92a5f112e..f0d96682de 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -47,8 +47,9 @@ lerna-debug.log* .env.production.local .env.local -# Local scripts +# Local scripts (tracked scripts are explicitly excluded below) scripts/ +!scripts/patch-schema-generator.js # temp directory .temp diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index f0cd5c24be..4db3884eb5 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -70,6 +70,7 @@ RUN cd packages/auth && bun run build \ # Generate Prisma schema for API and build NestJS app RUN cd packages/db && node scripts/combine-schemas.js \ && cp /app/packages/db/dist/schema.prisma /app/apps/api/prisma/schema.prisma \ + && node /app/apps/api/scripts/patch-schema-generator.js \ && cd /app/apps/api && bunx prisma generate && bunx nest build # ============================================================================= @@ -84,7 +85,7 @@ WORKDIR /app RUN chown nestjs:nestjs /app # Install runtime dependencies -RUN apt-get update && apt-get install -y --no-install-recommends wget openssl && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends wget && rm -rf /var/lib/apt/lists/* # Copy built NestJS app COPY --from=builder --chown=nestjs:nestjs /app/apps/api/dist ./dist diff --git a/apps/api/customPrismaExtension.ts b/apps/api/customPrismaExtension.ts index f1b5991b58..bca8da0220 100644 --- a/apps/api/customPrismaExtension.ts +++ b/apps/api/customPrismaExtension.ts @@ -33,8 +33,7 @@ export class PrismaExtension implements BuildExtension { constructor(private options: PrismaExtensionOptions) { this.moduleExternals = [ '@prisma/client', - '@prisma/engines', - '@trycompai/db', // Add the published package to externals + '@trycompai/db', ]; } @@ -123,7 +122,12 @@ export class PrismaExtension implements BuildExtension { await mkdir(schemaDestinationDir, { recursive: true }); await cp(schemaPath, schemaDestinationPath); - // Add prisma generate command to generate the client from the copied schema + // Patch the schema to use prisma-client-js (populates @prisma/client at runtime) + commands.push( + `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema.prisma`, + ); + + // Add prisma generate command to generate the client from the patched schema commands.push( `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma`, ); @@ -192,6 +196,14 @@ export class PrismaExtension implements BuildExtension { await mkdir(schemaDir, { recursive: true }); await cp(schemaSourcePath, schemaDestinationPath); + // Patch schema to use prisma-client-js (default output → @prisma/client) + const { readFileSync, writeFileSync } = await import('node:fs'); + let schemaContent = readFileSync(schemaDestinationPath, 'utf8'); + schemaContent = schemaContent + .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') + .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); + writeFileSync(schemaDestinationPath, schemaContent); + const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js'); if (existsSync(clientEntryPoint) && !process.env.TRIGGER_PRISMA_FORCE_GENERATE) { diff --git a/apps/api/package.json b/apps/api/package.json index f6e6794b27..c5332ca2ca 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -21,8 +21,9 @@ "@nestjs/platform-express": "^11.1.5", "@nestjs/swagger": "^11.2.0", "@nestjs/throttler": "^6.5.0", - "@prisma/client": "6.18.0", - "@prisma/instrumentation": "^6.13.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", + "@prisma/instrumentation": "7.6.0", "@react-email/components": "^0.0.41", "@react-email/render": "^2.0.4", "@thallesp/nestjs-better-auth": "^2.4.0", @@ -30,7 +31,7 @@ "@trigger.dev/sdk": "4.4.3", "@trycompai/auth": "workspace:*", "@trycompai/company": "workspace:*", - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "@trycompai/email": "workspace:*", "@trycompai/integration-platform": "workspace:*", "@upstash/ratelimit": "^2.0.8", @@ -54,7 +55,7 @@ "nanoid": "^5.1.6", "pdf-lib": "^1.17.1", "playwright-core": "^1.57.0", - "prisma": "6.18.0", + "prisma": "7.6.0", "react": "^19.1.1", "react-dom": "^19.1.0", "reflect-metadata": "^0.2.2", @@ -123,7 +124,7 @@ "build": "nest build", "build:docker": "bunx prisma generate && nest build", "db:generate": "bun run db:getschema && bunx prisma generate", - "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", + "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma && node scripts/patch-schema-generator.js", "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/api", "deploy:trigger-prod": "npx trigger.dev@4.4.3 deploy", "dev": "bunx concurrently --kill-others --names \"nest,trigger\" --prefix-colors \"green,blue\" \"nest start --watch\" \"trigger dev\"", diff --git a/apps/api/prisma.config.ts b/apps/api/prisma.config.ts new file mode 100644 index 0000000000..f0e7629866 --- /dev/null +++ b/apps/api/prisma.config.ts @@ -0,0 +1,9 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/apps/api/prisma/client.d.ts b/apps/api/prisma/client.d.ts deleted file mode 100644 index c9d2501a47..0000000000 --- a/apps/api/prisma/client.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -export declare const db: PrismaClient; diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index a696328bef..7a53dd537d 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -1,7 +1,13 @@ import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -export const db = globalForPrisma.prisma || new PrismaClient(); +function createPrismaClient(): PrismaClient { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const db = globalForPrisma.prisma || createPrismaClient(); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/apps/api/scripts/patch-schema-generator.js b/apps/api/scripts/patch-schema-generator.js new file mode 100644 index 0000000000..44580544ed --- /dev/null +++ b/apps/api/scripts/patch-schema-generator.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const schemaPath = path.join(__dirname, '../prisma/schema.prisma'); +let schema = fs.readFileSync(schemaPath, 'utf8'); + +// Surgically patch the generator block: swap provider and remove output line. +// Preserves all other generator settings (previewFeatures, etc.). +schema = schema + .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') + .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); + +fs.writeFileSync(schemaPath, schema); +console.log('[patch-schema] Set generator to prisma-client-js (default output)'); diff --git a/apps/api/src/admin-organizations/admin-context.controller.spec.ts b/apps/api/src/admin-organizations/admin-context.controller.spec.ts index 6a7e8b3a4f..cb836fb2b6 100644 --- a/apps/api/src/admin-organizations/admin-context.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-context.controller.spec.ts @@ -14,7 +14,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ db: {} })); +jest.mock('@db', () => ({ db: {} })); describe('AdminContextController', () => { let controller: AdminContextController; diff --git a/apps/api/src/admin-organizations/admin-evidence.controller.spec.ts b/apps/api/src/admin-organizations/admin-evidence.controller.spec.ts index 063c14c6f4..8b7ecb8709 100644 --- a/apps/api/src/admin-organizations/admin-evidence.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-evidence.controller.spec.ts @@ -15,7 +15,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ db: {} })); +jest.mock('@db', () => ({ db: {} })); describe('AdminEvidenceController', () => { let controller: AdminEvidenceController; diff --git a/apps/api/src/admin-organizations/admin-findings.controller.spec.ts b/apps/api/src/admin-organizations/admin-findings.controller.spec.ts index fb97301be8..a96cb6dd03 100644 --- a/apps/api/src/admin-organizations/admin-findings.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-findings.controller.spec.ts @@ -15,7 +15,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: {}, FindingStatus: { open: 'open', diff --git a/apps/api/src/admin-organizations/admin-findings.controller.ts b/apps/api/src/admin-organizations/admin-findings.controller.ts index cfffa6b143..d208cdf900 100644 --- a/apps/api/src/admin-organizations/admin-findings.controller.ts +++ b/apps/api/src/admin-organizations/admin-findings.controller.ts @@ -15,7 +15,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; -import { FindingStatus } from '@trycompai/db'; +import { FindingStatus } from '@db'; import { PlatformAdminGuard } from '../auth/platform-admin.guard'; import { FindingsService } from '../findings/findings.service'; import { CreateFindingDto } from '../findings/dto/create-finding.dto'; diff --git a/apps/api/src/admin-organizations/admin-guard-integration.spec.ts b/apps/api/src/admin-organizations/admin-guard-integration.spec.ts index 20e1491b4e..b00a032ac5 100644 --- a/apps/api/src/admin-organizations/admin-guard-integration.spec.ts +++ b/apps/api/src/admin-organizations/admin-guard-integration.spec.ts @@ -16,7 +16,7 @@ jest.mock('../auth/auth.server', () => ({ }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { user: { findUnique: (...args: unknown[]) => mockFindUnique(...args), diff --git a/apps/api/src/admin-organizations/admin-organizations.controller.spec.ts b/apps/api/src/admin-organizations/admin-organizations.controller.spec.ts index 70209326f1..2dc9c367e6 100644 --- a/apps/api/src/admin-organizations/admin-organizations.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-organizations.controller.spec.ts @@ -14,7 +14,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ db: {} })); +jest.mock('@db', () => ({ db: {} })); describe('AdminOrganizationsController', () => { let controller: AdminOrganizationsController; diff --git a/apps/api/src/admin-organizations/admin-organizations.service.spec.ts b/apps/api/src/admin-organizations/admin-organizations.service.spec.ts index 3c9cce64fd..9af5e5f0e3 100644 --- a/apps/api/src/admin-organizations/admin-organizations.service.spec.ts +++ b/apps/api/src/admin-organizations/admin-organizations.service.spec.ts @@ -1,7 +1,7 @@ import { NotFoundException, BadRequestException } from '@nestjs/common'; import { AdminOrganizationsService } from './admin-organizations.service'; -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { organization: { findMany: jest.fn(), @@ -33,7 +33,7 @@ jest.mock('../email/templates/invite-member', () => ({ InviteEmail: jest.fn().mockReturnValue(null), })); -import { db } from '@trycompai/db'; +import { db } from '@db'; const mockDb = db as jest.Mocked; diff --git a/apps/api/src/admin-organizations/admin-organizations.service.ts b/apps/api/src/admin-organizations/admin-organizations.service.ts index ab36ddf96b..57dbf78ca5 100644 --- a/apps/api/src/admin-organizations/admin-organizations.service.ts +++ b/apps/api/src/admin-organizations/admin-organizations.service.ts @@ -4,7 +4,7 @@ import { BadRequestException, Logger, } from '@nestjs/common'; -import { AuditLogEntityType, db } from '@trycompai/db'; +import { AuditLogEntityType, db } from '@db'; import { triggerEmail } from '../email/trigger-email'; import { InviteEmail } from '../email/templates/invite-member'; diff --git a/apps/api/src/admin-organizations/admin-policies.controller.spec.ts b/apps/api/src/admin-organizations/admin-policies.controller.spec.ts index c9263b5c2d..d3ec2b8e7e 100644 --- a/apps/api/src/admin-organizations/admin-policies.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-policies.controller.spec.ts @@ -15,7 +15,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { frameworkInstance: { findMany: jest.fn().mockResolvedValue([]) }, context: { findMany: jest.fn().mockResolvedValue([]) }, diff --git a/apps/api/src/admin-organizations/admin-policies.controller.ts b/apps/api/src/admin-organizations/admin-policies.controller.ts index 24f6fb1b72..1dcfa6c952 100644 --- a/apps/api/src/admin-organizations/admin-policies.controller.ts +++ b/apps/api/src/admin-organizations/admin-policies.controller.ts @@ -13,7 +13,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { PolicyStatus, Frequency, diff --git a/apps/api/src/admin-organizations/admin-security.spec.ts b/apps/api/src/admin-organizations/admin-security.spec.ts index a96a848588..4f95a5516f 100644 --- a/apps/api/src/admin-organizations/admin-security.spec.ts +++ b/apps/api/src/admin-organizations/admin-security.spec.ts @@ -16,7 +16,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: {}, FindingStatus: { open: 'open', ready_for_review: 'ready_for_review', needs_revision: 'needs_revision', closed: 'closed' }, FindingType: { soc2: 'soc2', iso27001: 'iso27001' }, diff --git a/apps/api/src/admin-organizations/admin-tasks.controller.spec.ts b/apps/api/src/admin-organizations/admin-tasks.controller.spec.ts index de7f51c1f2..2ae4a06caa 100644 --- a/apps/api/src/admin-organizations/admin-tasks.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-tasks.controller.spec.ts @@ -17,7 +17,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: {}, TaskStatus: { todo: 'todo', diff --git a/apps/api/src/admin-organizations/admin-tasks.controller.ts b/apps/api/src/admin-organizations/admin-tasks.controller.ts index f1b09d2436..946be61a63 100644 --- a/apps/api/src/admin-organizations/admin-tasks.controller.ts +++ b/apps/api/src/admin-organizations/admin-tasks.controller.ts @@ -21,7 +21,7 @@ import { CommentEntityType, AttachmentEntityType, db, -} from '@trycompai/db'; +} from '@db'; import { PlatformAdminGuard } from '../auth/platform-admin.guard'; import { TasksService } from '../tasks/tasks.service'; import { CommentsService } from '../comments/comments.service'; diff --git a/apps/api/src/admin-organizations/admin-vendors.controller.spec.ts b/apps/api/src/admin-organizations/admin-vendors.controller.spec.ts index 9541b90a0b..edc89b643b 100644 --- a/apps/api/src/admin-organizations/admin-vendors.controller.spec.ts +++ b/apps/api/src/admin-organizations/admin-vendors.controller.spec.ts @@ -14,7 +14,7 @@ jest.mock('../auth/auth.server', () => ({ auth: { api: {} }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: {}, VendorCategory: { cloud: 'cloud', diff --git a/apps/api/src/admin-organizations/admin-vendors.controller.ts b/apps/api/src/admin-organizations/admin-vendors.controller.ts index 56f97c9116..29cedae5d0 100644 --- a/apps/api/src/admin-organizations/admin-vendors.controller.ts +++ b/apps/api/src/admin-organizations/admin-vendors.controller.ts @@ -14,7 +14,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; -import { VendorCategory, VendorStatus } from '@trycompai/db'; +import { VendorCategory, VendorStatus } from '@db'; import { PlatformAdminGuard } from '../auth/platform-admin.guard'; import { VendorsService } from '../vendors/vendors.service'; import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor'; diff --git a/apps/api/src/admin-organizations/dto/create-admin-vendor.dto.ts b/apps/api/src/admin-organizations/dto/create-admin-vendor.dto.ts index 517291480c..c165fdf6d0 100644 --- a/apps/api/src/admin-organizations/dto/create-admin-vendor.dto.ts +++ b/apps/api/src/admin-organizations/dto/create-admin-vendor.dto.ts @@ -7,7 +7,7 @@ import { IsUrl, } from 'class-validator'; import { Transform } from 'class-transformer'; -import { VendorCategory, VendorStatus } from '@trycompai/db'; +import { VendorCategory, VendorStatus } from '@db'; export class CreateAdminVendorDto { @ApiProperty({ diff --git a/apps/api/src/assistant-chat/assistant-chat-tools.ts b/apps/api/src/assistant-chat/assistant-chat-tools.ts index eb4314a7e9..9d487e3e8b 100644 --- a/apps/api/src/assistant-chat/assistant-chat-tools.ts +++ b/apps/api/src/assistant-chat/assistant-chat-tools.ts @@ -1,4 +1,4 @@ -import { db, Departments, RiskCategory, RiskStatus } from '@trycompai/db'; +import { db, Departments, RiskCategory, RiskStatus } from '@db'; import { z } from 'zod'; type Permissions = Record; diff --git a/apps/api/src/audit/audit-log.controller.spec.ts b/apps/api/src/audit/audit-log.controller.spec.ts index 9d10c04d91..3ce604b749 100644 --- a/apps/api/src/audit/audit-log.controller.spec.ts +++ b/apps/api/src/audit/audit-log.controller.spec.ts @@ -16,7 +16,7 @@ jest.mock('@trycompai/auth', () => ({ })); const mockFindMany = jest.fn(); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { auditLog: { findMany: (...args: unknown[]) => mockFindMany(...args), @@ -36,7 +36,9 @@ describe('AuditLogController', () => { userEmail: 'user@example.com', organizationId: 'org_1', memberId: 'mem_1', - permissions: [], + isApiKey: false, + isPlatformAdmin: false, + userRoles: null, }; beforeEach(async () => { @@ -237,8 +239,10 @@ describe('AuditLogController', () => { const authContextNoUser: AuthContextType = { authType: 'api-key' as const, organizationId: 'org_1', - permissions: [], - } as AuthContextType; + isApiKey: true, + isPlatformAdmin: false, + userRoles: null, + }; const result = await controller.getAuditLogs( 'org_1', diff --git a/apps/api/src/audit/audit-log.controller.ts b/apps/api/src/audit/audit-log.controller.ts index ea868c968f..f950aeea97 100644 --- a/apps/api/src/audit/audit-log.controller.ts +++ b/apps/api/src/audit/audit-log.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiSecurity, ApiTags } from '@nestjs/swagger'; -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { AuthContext, OrganizationId } from '../auth/auth-context.decorator'; import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import { PermissionGuard } from '../auth/permission.guard'; diff --git a/apps/api/src/auth/api-key.service.ts b/apps/api/src/auth/api-key.service.ts index 01551f33f4..b0fbac86de 100644 --- a/apps/api/src/auth/api-key.service.ts +++ b/apps/api/src/auth/api-key.service.ts @@ -4,7 +4,7 @@ import { NotFoundException, BadRequestException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { statement } from '@trycompai/auth'; import { createHash, randomBytes } from 'node:crypto'; diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index bc1c57c15a..cfc074be26 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -9,7 +9,7 @@ import { UseGuards, } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { OrganizationId } from './auth-context.decorator'; import { PermissionGuard } from './permission.guard'; import { RequirePermission } from './require-permission.decorator'; diff --git a/apps/api/src/auth/auth.server.ts b/apps/api/src/auth/auth.server.ts index bfcd3ed1e6..865c84573a 100644 --- a/apps/api/src/auth/auth.server.ts +++ b/apps/api/src/auth/auth.server.ts @@ -2,7 +2,7 @@ import '../config/load-env'; import { MagicLinkEmail, OTPVerificationEmail } from '@trycompai/email'; import { triggerEmail } from '../email/trigger-email'; import { InviteEmail } from '../email/templates/invite-member'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; import { @@ -49,6 +49,7 @@ export function getTrustedOrigins(): string[] { 'http://localhost:3002', 'http://localhost:3333', 'http://localhost:3004', + 'http://localhost:3008', 'https://app.trycomp.ai', 'https://portal.trycomp.ai', 'https://api.trycomp.ai', diff --git a/apps/api/src/auth/hybrid-auth.guard.ts b/apps/api/src/auth/hybrid-auth.guard.ts index f67690066b..adbbb23cde 100644 --- a/apps/api/src/auth/hybrid-auth.guard.ts +++ b/apps/api/src/auth/hybrid-auth.guard.ts @@ -6,7 +6,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { ApiKeyService } from './api-key.service'; import { auth } from './auth.server'; import { IS_PUBLIC_KEY } from './public.decorator'; diff --git a/apps/api/src/auth/platform-admin.guard.spec.ts b/apps/api/src/auth/platform-admin.guard.spec.ts index bf80815bdb..a5ff714c02 100644 --- a/apps/api/src/auth/platform-admin.guard.spec.ts +++ b/apps/api/src/auth/platform-admin.guard.spec.ts @@ -16,7 +16,7 @@ jest.mock('./auth.server', () => ({ }, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { user: { findUnique: (...args: unknown[]) => mockFindUnique(...args), diff --git a/apps/api/src/auth/platform-admin.guard.ts b/apps/api/src/auth/platform-admin.guard.ts index 509b756c3b..d84ccc96dd 100644 --- a/apps/api/src/auth/platform-admin.guard.ts +++ b/apps/api/src/auth/platform-admin.guard.ts @@ -5,7 +5,7 @@ import { UnauthorizedException, ForbiddenException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { auth } from './auth.server'; interface PlatformAdminRequest { diff --git a/apps/api/src/auth/types.ts b/apps/api/src/auth/types.ts index dbfbd5ac76..4e7933d53e 100644 --- a/apps/api/src/auth/types.ts +++ b/apps/api/src/auth/types.ts @@ -1,6 +1,6 @@ // Types for API authentication - supports API keys and session-based auth -import { Departments } from '@prisma/client'; +import { Departments } from '@db'; export interface AuthenticatedRequest extends Request { organizationId: string; diff --git a/apps/api/src/browserbase/browserbase.service.ts b/apps/api/src/browserbase/browserbase.service.ts index ee208b3cd8..dc6a7176b8 100644 --- a/apps/api/src/browserbase/browserbase.service.ts +++ b/apps/api/src/browserbase/browserbase.service.ts @@ -3,7 +3,7 @@ import Browserbase from '@browserbasehq/sdk'; // Lazy-imported in createStagehand() to avoid Node v25 crash // (SlowBuffer.prototype was removed — @browserbasehq/stagehand bundles buffer-equal-constant-time which uses it) type Stagehand = import('@browserbasehq/stagehand').Stagehand; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { z } from 'zod'; import { GetObjectCommand, diff --git a/apps/api/src/cloud-security/cloud-security-legacy.service.ts b/apps/api/src/cloud-security/cloud-security-legacy.service.ts index 28f9de3eb1..47fe1d94f9 100644 --- a/apps/api/src/cloud-security/cloud-security-legacy.service.ts +++ b/apps/api/src/cloud-security/cloud-security-legacy.service.ts @@ -5,7 +5,7 @@ import { BadRequestException, } from '@nestjs/common'; import { db } from '@db'; -import { Prisma } from '@prisma/client'; +import { Prisma } from '@db'; import { createCipheriv, randomBytes, diff --git a/apps/api/src/comments/comments.controller.spec.ts b/apps/api/src/comments/comments.controller.spec.ts index 4f93c1dfb9..1d53e22fd5 100644 --- a/apps/api/src/comments/comments.controller.spec.ts +++ b/apps/api/src/comments/comments.controller.spec.ts @@ -7,6 +7,12 @@ import { CommentsController } from './comments.controller'; import { CommentsService } from './comments.service'; // Mock auth.server to avoid importing better-auth ESM in Jest +jest.mock('@db', () => ({ + ...jest.requireActual('@prisma/client'), + db: {}, + Prisma: { PrismaClientKnownRequestError: class PrismaClientKnownRequestError extends Error { code: string; constructor(message: string, { code }: { code: string }) { super(message); this.code = code; } } }, +})); + jest.mock('../auth/auth.server', () => ({ auth: { api: { getSession: jest.fn() } }, })); @@ -41,7 +47,7 @@ describe('CommentsController', () => { const apiKeyAuthContext: AuthContext = { organizationId: 'org_123', - authType: 'apiKey', + authType: 'api-key', isApiKey: true, isPlatformAdmin: false, userId: undefined, diff --git a/apps/api/src/comments/comments.service.ts b/apps/api/src/comments/comments.service.ts index 9aea2b8e7b..40f79a03d0 100644 --- a/apps/api/src/comments/comments.service.ts +++ b/apps/api/src/comments/comments.service.ts @@ -5,7 +5,7 @@ import { InternalServerErrorException, Logger, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { AttachmentsService } from '../attachments/attachments.service'; import { AttachmentResponseDto, diff --git a/apps/api/src/context/context.service.ts b/apps/api/src/context/context.service.ts index 6fb771552b..b6ff83f9e7 100644 --- a/apps/api/src/context/context.service.ts +++ b/apps/api/src/context/context.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { CreateContextDto } from './dto/create-context.dto'; import { UpdateContextDto } from './dto/update-context.dto'; diff --git a/apps/api/src/controls/controls.service.ts b/apps/api/src/controls/controls.service.ts index 7950c40416..c349ebb705 100644 --- a/apps/api/src/controls/controls.service.ts +++ b/apps/api/src/controls/controls.service.ts @@ -2,7 +2,7 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { CreateControlDto } from './dto/create-control.dto'; const controlInclude = { diff --git a/apps/api/src/device-agent/device-agent-auth.service.spec.ts b/apps/api/src/device-agent/device-agent-auth.service.spec.ts index ac0d724d12..7025e899a5 100644 --- a/apps/api/src/device-agent/device-agent-auth.service.spec.ts +++ b/apps/api/src/device-agent/device-agent-auth.service.spec.ts @@ -2,7 +2,7 @@ import { ForbiddenException, NotFoundException, UnauthorizedException } from '@n import { DeviceAgentAuthService } from './device-agent-auth.service'; // Mock dependencies -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { member: { findMany: jest.fn(), @@ -36,7 +36,7 @@ jest.mock('./device-agent-kv', () => ({ }, })); -import { db } from '@trycompai/db'; +import { db } from '@db'; import { auth } from '../auth/auth.server'; import { deviceAgentRedisClient } from './device-agent-kv'; @@ -54,7 +54,7 @@ describe('DeviceAgentAuthService', () => { describe('generateAuthCode', () => { it('should generate an auth code and store it in KV', async () => { - (mockAuth.api.getSession as jest.Mock).mockResolvedValue({ + (mockAuth.api.getSession as unknown as jest.Mock).mockResolvedValue({ user: { id: 'user-1' }, session: { token: 'raw-session-token' }, }); @@ -77,7 +77,7 @@ describe('DeviceAgentAuthService', () => { }); it('should throw UnauthorizedException if no session', async () => { - (mockAuth.api.getSession as jest.Mock).mockResolvedValue(null); + (mockAuth.api.getSession as unknown as jest.Mock).mockResolvedValue(null); const headers = new Headers(); await expect( diff --git a/apps/api/src/device-agent/device-agent-auth.service.ts b/apps/api/src/device-agent/device-agent-auth.service.ts index 9be5f8d1da..65bf2955bc 100644 --- a/apps/api/src/device-agent/device-agent-auth.service.ts +++ b/apps/api/src/device-agent/device-agent-auth.service.ts @@ -6,7 +6,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { randomBytes } from 'node:crypto'; -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { auth } from '../auth/auth.server'; import { deviceAgentRedisClient } from './device-agent-kv'; import { diff --git a/apps/api/src/device-agent/device-registration.helpers.ts b/apps/api/src/device-agent/device-registration.helpers.ts index bc38d99132..e170c4e8b9 100644 --- a/apps/api/src/device-agent/device-registration.helpers.ts +++ b/apps/api/src/device-agent/device-registration.helpers.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'node:crypto'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { RegisterDeviceDto } from './dto/register-device.dto'; interface MemberRef { diff --git a/apps/api/src/devices/devices.controller.spec.ts b/apps/api/src/devices/devices.controller.spec.ts index 579121c211..7d03773005 100644 --- a/apps/api/src/devices/devices.controller.spec.ts +++ b/apps/api/src/devices/devices.controller.spec.ts @@ -5,6 +5,12 @@ import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import { PermissionGuard } from '../auth/permission.guard'; import type { AuthContext as AuthContextType } from '../auth/types'; +jest.mock('@db', () => ({ + ...jest.requireActual('@prisma/client'), + db: {}, + Prisma: { PrismaClientKnownRequestError: class PrismaClientKnownRequestError extends Error { code: string; constructor(message: string, { code }: { code: string }) { super(message); this.code = code; } } }, +})); + jest.mock('../auth/auth.server', () => ({ auth: { api: { getSession: jest.fn() } }, })); @@ -34,7 +40,9 @@ describe('DevicesController', () => { userEmail: 'user@example.com', organizationId: 'org_1', memberId: 'mem_1', - permissions: [], + isApiKey: false, + isPlatformAdmin: false, + userRoles: null, }; beforeEach(async () => { @@ -92,8 +100,10 @@ describe('DevicesController', () => { const authContextNoUser: AuthContextType = { authType: 'api-key' as const, organizationId: 'org_1', - permissions: [], - } as AuthContextType; + isApiKey: true, + isPlatformAdmin: false, + userRoles: null, + }; const result = await controller.getAllDevices('org_1', authContextNoUser); @@ -154,8 +164,10 @@ describe('DevicesController', () => { const authContextNoUser: AuthContextType = { authType: 'api-key' as const, organizationId: 'org_1', - permissions: [], - } as AuthContextType; + isApiKey: true, + isPlatformAdmin: false, + userRoles: null, + }; const result = await controller.getDevicesByMember( 'mem_1', diff --git a/apps/api/src/devices/devices.service.ts b/apps/api/src/devices/devices.service.ts index 4ec466013c..fcfd5e4eef 100644 --- a/apps/api/src/devices/devices.service.ts +++ b/apps/api/src/devices/devices.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { FleetService } from '../lib/fleet.service'; import { DeviceResponseDto } from './dto/device-responses.dto'; import type { MemberResponseDto } from './dto/member-responses.dto'; diff --git a/apps/api/src/evidence-forms/evidence-forms.service.spec.ts b/apps/api/src/evidence-forms/evidence-forms.service.spec.ts index d80a918270..9a02a59303 100644 --- a/apps/api/src/evidence-forms/evidence-forms.service.spec.ts +++ b/apps/api/src/evidence-forms/evidence-forms.service.spec.ts @@ -1,7 +1,7 @@ import type { AuthContext } from '@/auth/types'; import { EvidenceFormsService } from './evidence-forms.service'; import type { AttachmentsService } from '@/attachments/attachments.service'; -import { db } from '@trycompai/db'; +import { db } from '@db'; jest.mock( '@/attachments/attachments.service', @@ -11,7 +11,7 @@ jest.mock( { virtual: true }, ); -jest.mock('@trycompai/db', () => { +jest.mock('@db', () => { const evidenceFormTypeEnum = { board_meeting: 'board_meeting', it_leadership_meeting: 'it_leadership_meeting', diff --git a/apps/api/src/evidence-forms/evidence-forms.service.ts b/apps/api/src/evidence-forms/evidence-forms.service.ts index 050110c410..01408529de 100644 --- a/apps/api/src/evidence-forms/evidence-forms.service.ts +++ b/apps/api/src/evidence-forms/evidence-forms.service.ts @@ -1,6 +1,6 @@ import { AttachmentsService } from '@/attachments/attachments.service'; import type { AuthContext } from '@/auth/types'; -import { db, EvidenceFormType as DbEvidenceFormType } from '@trycompai/db'; +import { db, EvidenceFormType as DbEvidenceFormType } from '@db'; import { toDbEvidenceFormType, toExternalEvidenceFormType, diff --git a/apps/api/src/finding-template/finding-template.service.ts b/apps/api/src/finding-template/finding-template.service.ts index 0bda020683..09487cfb66 100644 --- a/apps/api/src/finding-template/finding-template.service.ts +++ b/apps/api/src/finding-template/finding-template.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { CreateFindingTemplateDto } from './dto/create-finding-template.dto'; import { UpdateFindingTemplateDto } from './dto/update-finding-template.dto'; diff --git a/apps/api/src/findings/dto/create-finding.dto.ts b/apps/api/src/findings/dto/create-finding.dto.ts index 8bc26f22b7..33d6766ec1 100644 --- a/apps/api/src/findings/dto/create-finding.dto.ts +++ b/apps/api/src/findings/dto/create-finding.dto.ts @@ -7,7 +7,7 @@ import { IsOptional, MaxLength, } from 'class-validator'; -import { FindingType } from '@trycompai/db'; +import { FindingType } from '@db'; import { evidenceFormTypeSchema, type EvidenceFormType, diff --git a/apps/api/src/findings/dto/update-finding.dto.ts b/apps/api/src/findings/dto/update-finding.dto.ts index bb2fb047d3..3299d232a2 100644 --- a/apps/api/src/findings/dto/update-finding.dto.ts +++ b/apps/api/src/findings/dto/update-finding.dto.ts @@ -6,7 +6,7 @@ import { IsNotEmpty, MaxLength, } from 'class-validator'; -import { FindingStatus, FindingType } from '@trycompai/db'; +import { FindingStatus, FindingType } from '@db'; export class UpdateFindingDto { @ApiProperty({ diff --git a/apps/api/src/findings/findings.controller.spec.ts b/apps/api/src/findings/findings.controller.spec.ts index 0fa9a69467..2010d9b328 100644 --- a/apps/api/src/findings/findings.controller.spec.ts +++ b/apps/api/src/findings/findings.controller.spec.ts @@ -27,7 +27,7 @@ jest.mock( { virtual: true }, ); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ FindingType: { soc2: 'soc2', iso27001: 'iso27001', diff --git a/apps/api/src/findings/findings.controller.ts b/apps/api/src/findings/findings.controller.ts index 79037c4910..2f0d3dc9ed 100644 --- a/apps/api/src/findings/findings.controller.ts +++ b/apps/api/src/findings/findings.controller.ts @@ -22,7 +22,7 @@ import { ApiTags, ApiSecurity, } from '@nestjs/swagger'; -import { FindingStatus } from '@trycompai/db'; +import { FindingStatus } from '@db'; import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import { PermissionGuard } from '../auth/permission.guard'; import { RequirePermission } from '../auth/require-permission.decorator'; @@ -32,7 +32,7 @@ import { FindingsService } from './findings.service'; import { CreateFindingDto } from './dto/create-finding.dto'; import { UpdateFindingDto } from './dto/update-finding.dto'; import { ValidateFindingIdPipe } from './pipes/validate-finding-id.pipe'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { evidenceFormTypeSchema } from '@/evidence-forms/evidence-forms.definitions'; @ApiTags('Findings') diff --git a/apps/api/src/findings/findings.service.ts b/apps/api/src/findings/findings.service.ts index fd62476fb6..a8864b39dc 100644 --- a/apps/api/src/findings/findings.service.ts +++ b/apps/api/src/findings/findings.service.ts @@ -10,7 +10,7 @@ import { EvidenceFormType as DbEvidenceFormType, FindingStatus, FindingType, -} from '@trycompai/db'; +} from '@db'; import { toDbEvidenceFormType, toExternalEvidenceFormType, diff --git a/apps/api/src/framework-editor/control-template/control-template.service.ts b/apps/api/src/framework-editor/control-template/control-template.service.ts index 12c8ef0253..16f346a141 100644 --- a/apps/api/src/framework-editor/control-template/control-template.service.ts +++ b/apps/api/src/framework-editor/control-template/control-template.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common'; -import { db, Prisma } from '@trycompai/db'; -import type { EvidenceFormType } from '@trycompai/db'; +import { db, Prisma } from '@db'; +import type { EvidenceFormType } from '@db'; import { CreateControlTemplateDto } from './dto/create-control-template.dto'; import { UpdateControlTemplateDto } from './dto/update-control-template.dto'; diff --git a/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts b/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts index e0d698681a..1cede340f7 100644 --- a/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts +++ b/apps/api/src/framework-editor/framework/dto/import-framework.dto.ts @@ -19,7 +19,7 @@ import { Frequency, Departments, TaskAutomationStatus, -} from '@trycompai/db'; +} from '@db'; import { MaxJsonSize } from '../../validators/max-json-size.validator'; class ImportFrameworkMetaDto { diff --git a/apps/api/src/framework-editor/framework/framework-export.service.ts b/apps/api/src/framework-editor/framework/framework-export.service.ts index a2273d3f46..ba2455bd49 100644 --- a/apps/api/src/framework-editor/framework/framework-export.service.ts +++ b/apps/api/src/framework-editor/framework/framework-export.service.ts @@ -4,7 +4,7 @@ import { BadRequestException, Logger, } from '@nestjs/common'; -import { db, Prisma, EvidenceFormType } from '@trycompai/db'; +import { db, Prisma, EvidenceFormType } from '@db'; import type { ImportFrameworkDto } from './dto/import-framework.dto'; export interface ExportedFramework { diff --git a/apps/api/src/framework-editor/framework/framework.service.ts b/apps/api/src/framework-editor/framework/framework.service.ts index bfa2388a1e..4e9e3736d4 100644 --- a/apps/api/src/framework-editor/framework/framework.service.ts +++ b/apps/api/src/framework-editor/framework/framework.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common'; -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { CreateFrameworkDto } from './dto/create-framework.dto'; import { UpdateFrameworkDto } from './dto/update-framework.dto'; diff --git a/apps/api/src/framework-editor/policy-template/dto/create-policy-template.dto.ts b/apps/api/src/framework-editor/policy-template/dto/create-policy-template.dto.ts index a7d87c6c7f..9f744ec8d7 100644 --- a/apps/api/src/framework-editor/policy-template/dto/create-policy-template.dto.ts +++ b/apps/api/src/framework-editor/policy-template/dto/create-policy-template.dto.ts @@ -5,7 +5,7 @@ import { IsEnum, MaxLength, } from 'class-validator'; -import { Frequency, Departments } from '@trycompai/db'; +import { Frequency, Departments } from '@db'; export class CreatePolicyTemplateDto { @ApiProperty({ example: 'Information Security Policy' }) diff --git a/apps/api/src/framework-editor/policy-template/policy-template.service.ts b/apps/api/src/framework-editor/policy-template/policy-template.service.ts index ec7fce47c9..4b60ba77d6 100644 --- a/apps/api/src/framework-editor/policy-template/policy-template.service.ts +++ b/apps/api/src/framework-editor/policy-template/policy-template.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common'; -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { CreatePolicyTemplateDto } from './dto/create-policy-template.dto'; import { UpdatePolicyTemplateDto } from './dto/update-policy-template.dto'; diff --git a/apps/api/src/framework-editor/requirement/requirement.service.ts b/apps/api/src/framework-editor/requirement/requirement.service.ts index 6816bda72c..0f0067e513 100644 --- a/apps/api/src/framework-editor/requirement/requirement.service.ts +++ b/apps/api/src/framework-editor/requirement/requirement.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { CreateRequirementDto } from './dto/create-requirement.dto'; import { UpdateRequirementDto } from './dto/update-requirement.dto'; diff --git a/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts b/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts index 27288b52dc..ca3e1ab113 100644 --- a/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts +++ b/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts @@ -6,7 +6,7 @@ import { IsOptional, MaxLength, } from 'class-validator'; -import { Frequency, Departments } from '@trycompai/db'; +import { Frequency, Departments } from '@db'; export class CreateTaskTemplateDto { @ApiProperty({ diff --git a/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts b/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts index e6b567e570..af57ccb51d 100644 --- a/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts +++ b/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Frequency, Departments } from '@trycompai/db'; +import { Frequency, Departments } from '@db'; export class TaskTemplateResponseDto { @ApiProperty({ diff --git a/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts b/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts index 112800df89..d37ef01d91 100644 --- a/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts +++ b/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional, PartialType } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { TaskAutomationStatus } from '@trycompai/db'; +import { TaskAutomationStatus } from '@db'; import { CreateTaskTemplateDto } from './create-task-template.dto'; export class UpdateTaskTemplateDto extends PartialType(CreateTaskTemplateDto) { diff --git a/apps/api/src/framework-editor/task-template/task-template.service.ts b/apps/api/src/framework-editor/task-template/task-template.service.ts index f72c3af524..bf18256e88 100644 --- a/apps/api/src/framework-editor/task-template/task-template.service.ts +++ b/apps/api/src/framework-editor/task-template/task-template.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db, Frequency, Departments } from '@trycompai/db'; +import { db, Frequency, Departments } from '@db'; import { CreateTaskTemplateDto } from './dto/create-task-template.dto'; import { UpdateTaskTemplateDto } from './dto/update-task-template.dto'; diff --git a/apps/api/src/frameworks/frameworks-scores.helper.ts b/apps/api/src/frameworks/frameworks-scores.helper.ts index 9fbe6e4065..eb164b80e7 100644 --- a/apps/api/src/frameworks/frameworks-scores.helper.ts +++ b/apps/api/src/frameworks/frameworks-scores.helper.ts @@ -4,7 +4,7 @@ import { toDbEvidenceFormType, toExternalEvidenceFormType, } from '@trycompai/company'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { filterComplianceMembers } from '../utils/compliance-filters'; const SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1000; diff --git a/apps/api/src/frameworks/frameworks-upsert.helper.ts b/apps/api/src/frameworks/frameworks-upsert.helper.ts index 1f47846a96..6d94e15220 100644 --- a/apps/api/src/frameworks/frameworks-upsert.helper.ts +++ b/apps/api/src/frameworks/frameworks-upsert.helper.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@trycompai/db'; +import { Prisma } from '@db'; /** * Unwraps a `{ set: [...] }` wrapper that was incorrectly stored by a diff --git a/apps/api/src/frameworks/frameworks.service.spec.ts b/apps/api/src/frameworks/frameworks.service.spec.ts index dcf6047354..b3dba39007 100644 --- a/apps/api/src/frameworks/frameworks.service.spec.ts +++ b/apps/api/src/frameworks/frameworks.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundException } from '@nestjs/common'; import { FrameworksService } from './frameworks.service'; -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { frameworkInstance: { findMany: jest.fn(), @@ -18,7 +18,7 @@ jest.mock('./frameworks-scores.helper', () => ({ computeFrameworkComplianceScore: jest.fn(), })); -import { db } from '@trycompai/db'; +import { db } from '@db'; import { getOverviewScores, getCurrentMember, diff --git a/apps/api/src/frameworks/frameworks.service.ts b/apps/api/src/frameworks/frameworks.service.ts index a00f341af7..149a5f8b04 100644 --- a/apps/api/src/frameworks/frameworks.service.ts +++ b/apps/api/src/frameworks/frameworks.service.ts @@ -3,7 +3,7 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { db, type EvidenceFormType } from '@trycompai/db'; +import { db, type EvidenceFormType } from '@db'; import { getOverviewScores, getCurrentMember, diff --git a/apps/api/src/integration-platform/controllers/checks.controller.ts b/apps/api/src/integration-platform/controllers/checks.controller.ts index 66bcfcdfd5..e2e7881e2f 100644 --- a/apps/api/src/integration-platform/controllers/checks.controller.ts +++ b/apps/api/src/integration-platform/controllers/checks.controller.ts @@ -10,7 +10,7 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiSecurity } from '@nestjs/swagger'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; import { HybridAuthGuard } from '../../auth/hybrid-auth.guard'; import { PermissionGuard } from '../../auth/permission.guard'; import { RequirePermission } from '../../auth/require-permission.decorator'; diff --git a/apps/api/src/integration-platform/controllers/dynamic-integrations.controller.ts b/apps/api/src/integration-platform/controllers/dynamic-integrations.controller.ts index 6869bc6800..f906bbad65 100644 --- a/apps/api/src/integration-platform/controllers/dynamic-integrations.controller.ts +++ b/apps/api/src/integration-platform/controllers/dynamic-integrations.controller.ts @@ -12,7 +12,7 @@ import { Logger, UseGuards, } from '@nestjs/common'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; import { db } from '@db'; import { InternalTokenGuard } from '../../auth/internal-token.guard'; import { DynamicIntegrationRepository } from '../repositories/dynamic-integration.repository'; diff --git a/apps/api/src/integration-platform/controllers/sync.controller.ts b/apps/api/src/integration-platform/controllers/sync.controller.ts index 542ef9c6ad..ae43e1afb4 100644 --- a/apps/api/src/integration-platform/controllers/sync.controller.ts +++ b/apps/api/src/integration-platform/controllers/sync.controller.ts @@ -20,7 +20,7 @@ import { } from '../../auth/auth-context.decorator'; import type { AuthContext as AuthContextType } from '../../auth/types'; import { db } from '@db'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; import { ConnectionRepository } from '../repositories/connection.repository'; import { CredentialVaultService } from '../services/credential-vault.service'; import { OAuthCredentialsService } from '../services/oauth-credentials.service'; diff --git a/apps/api/src/integration-platform/controllers/task-integrations.controller.ts b/apps/api/src/integration-platform/controllers/task-integrations.controller.ts index 3d7a6d3887..0a9383181d 100644 --- a/apps/api/src/integration-platform/controllers/task-integrations.controller.ts +++ b/apps/api/src/integration-platform/controllers/task-integrations.controller.ts @@ -29,7 +29,7 @@ import { CredentialVaultService } from '../services/credential-vault.service'; import { OAuthCredentialsService } from '../services/oauth-credentials.service'; import { getStringValue, toStringCredentials } from '../utils/credential-utils'; import { db } from '@db'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; interface TaskIntegrationCheck { integrationId: string; diff --git a/apps/api/src/integration-platform/interceptors/platform-audit-log.interceptor.ts b/apps/api/src/integration-platform/interceptors/platform-audit-log.interceptor.ts index 015fd6a1be..7aaf00653f 100644 --- a/apps/api/src/integration-platform/interceptors/platform-audit-log.interceptor.ts +++ b/apps/api/src/integration-platform/interceptors/platform-audit-log.interceptor.ts @@ -5,7 +5,7 @@ import { Logger, NestInterceptor, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { Observable, tap } from 'rxjs'; import { MUTATION_METHODS } from '../../audit/audit-log.constants'; diff --git a/apps/api/src/integration-platform/repositories/check-run.repository.ts b/apps/api/src/integration-platform/repositories/check-run.repository.ts index c95bd20600..81b955b13f 100644 --- a/apps/api/src/integration-platform/repositories/check-run.repository.ts +++ b/apps/api/src/integration-platform/repositories/check-run.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { IntegrationRunStatus, Prisma } from '@prisma/client'; +import type { IntegrationRunStatus, Prisma } from '@db'; export interface CreateCheckRunDto { connectionId: string; diff --git a/apps/api/src/integration-platform/repositories/connection.repository.ts b/apps/api/src/integration-platform/repositories/connection.repository.ts index 82abbea119..ff27f64670 100644 --- a/apps/api/src/integration-platform/repositories/connection.repository.ts +++ b/apps/api/src/integration-platform/repositories/connection.repository.ts @@ -3,7 +3,7 @@ import { db } from '@db'; import type { IntegrationConnection, IntegrationConnectionStatus, -} from '@prisma/client'; +} from '@db'; export interface CreateConnectionDto { providerId: string; diff --git a/apps/api/src/integration-platform/repositories/credential.repository.ts b/apps/api/src/integration-platform/repositories/credential.repository.ts index 108b277ed9..c5c48b146c 100644 --- a/apps/api/src/integration-platform/repositories/credential.repository.ts +++ b/apps/api/src/integration-platform/repositories/credential.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { IntegrationCredentialVersion } from '@prisma/client'; +import type { IntegrationCredentialVersion } from '@db'; export interface CreateCredentialVersionDto { connectionId: string; diff --git a/apps/api/src/integration-platform/repositories/dynamic-check.repository.ts b/apps/api/src/integration-platform/repositories/dynamic-check.repository.ts index 275c4c6f7d..d15dcb32d8 100644 --- a/apps/api/src/integration-platform/repositories/dynamic-check.repository.ts +++ b/apps/api/src/integration-platform/repositories/dynamic-check.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { DynamicCheck, Prisma } from '@prisma/client'; +import type { DynamicCheck, Prisma } from '@db'; @Injectable() export class DynamicCheckRepository { diff --git a/apps/api/src/integration-platform/repositories/dynamic-integration.repository.ts b/apps/api/src/integration-platform/repositories/dynamic-integration.repository.ts index 9e23e222c8..f3d6e753ed 100644 --- a/apps/api/src/integration-platform/repositories/dynamic-integration.repository.ts +++ b/apps/api/src/integration-platform/repositories/dynamic-integration.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import { Prisma } from '@prisma/client'; -import type { DynamicIntegration, DynamicCheck } from '@prisma/client'; +import { Prisma } from '@db'; +import type { DynamicIntegration, DynamicCheck } from '@db'; export type DynamicIntegrationWithChecks = DynamicIntegration & { checks: DynamicCheck[]; diff --git a/apps/api/src/integration-platform/repositories/oauth-app.repository.ts b/apps/api/src/integration-platform/repositories/oauth-app.repository.ts index 465b9d87c3..f6b337e8ef 100644 --- a/apps/api/src/integration-platform/repositories/oauth-app.repository.ts +++ b/apps/api/src/integration-platform/repositories/oauth-app.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { IntegrationOAuthApp, Prisma } from '@prisma/client'; +import type { IntegrationOAuthApp, Prisma } from '@db'; export interface CreateOAuthAppDto { providerSlug: string; diff --git a/apps/api/src/integration-platform/repositories/oauth-state.repository.ts b/apps/api/src/integration-platform/repositories/oauth-state.repository.ts index ebce5d1ae2..675b0994da 100644 --- a/apps/api/src/integration-platform/repositories/oauth-state.repository.ts +++ b/apps/api/src/integration-platform/repositories/oauth-state.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { IntegrationOAuthState } from '@prisma/client'; +import type { IntegrationOAuthState } from '@db'; import { randomBytes } from 'crypto'; export interface CreateOAuthStateDto { diff --git a/apps/api/src/integration-platform/repositories/platform-credential.repository.ts b/apps/api/src/integration-platform/repositories/platform-credential.repository.ts index 2d5b718a0c..d0aa046490 100644 --- a/apps/api/src/integration-platform/repositories/platform-credential.repository.ts +++ b/apps/api/src/integration-platform/repositories/platform-credential.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; export interface CreatePlatformCredentialDto { providerSlug: string; diff --git a/apps/api/src/integration-platform/repositories/provider.repository.ts b/apps/api/src/integration-platform/repositories/provider.repository.ts index 620e79566a..bb0eb6b06d 100644 --- a/apps/api/src/integration-platform/repositories/provider.repository.ts +++ b/apps/api/src/integration-platform/repositories/provider.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { db } from '@db'; -import type { IntegrationProvider } from '@prisma/client'; +import type { IntegrationProvider } from '@db'; export interface CreateProviderDto { slug: string; diff --git a/apps/api/src/integration-platform/services/connection.service.ts b/apps/api/src/integration-platform/services/connection.service.ts index b854e9f26f..259bd89e3b 100644 --- a/apps/api/src/integration-platform/services/connection.service.ts +++ b/apps/api/src/integration-platform/services/connection.service.ts @@ -10,7 +10,7 @@ import { ConnectionAuthTeardownService } from './connection-auth-teardown.servic import type { IntegrationConnection, IntegrationConnectionStatus, -} from '@prisma/client'; +} from '@db'; export interface CreateConnectionInput { providerSlug: string; diff --git a/apps/api/src/integration-platform/services/dynamic-manifest-loader.service.ts b/apps/api/src/integration-platform/services/dynamic-manifest-loader.service.ts index 50d3f0e22f..ee84c02cfa 100644 --- a/apps/api/src/integration-platform/services/dynamic-manifest-loader.service.ts +++ b/apps/api/src/integration-platform/services/dynamic-manifest-loader.service.ts @@ -10,7 +10,7 @@ import { type CheckVariable, } from '@trycompai/integration-platform'; import { DynamicIntegrationRepository, type DynamicIntegrationWithChecks } from '../repositories/dynamic-integration.repository'; -import type { DynamicCheck } from '@prisma/client'; +import type { DynamicCheck } from '@db'; @Injectable() export class DynamicManifestLoaderService implements OnModuleInit { diff --git a/apps/api/src/integration-platform/services/generic-employee-sync.service.ts b/apps/api/src/integration-platform/services/generic-employee-sync.service.ts index c6c4796f41..c42959d891 100644 --- a/apps/api/src/integration-platform/services/generic-employee-sync.service.ts +++ b/apps/api/src/integration-platform/services/generic-employee-sync.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import type { SyncEmployee } from '@trycompai/integration-platform'; // ============================================================================ diff --git a/apps/api/src/integration-platform/services/integration-sync-logger.service.ts b/apps/api/src/integration-platform/services/integration-sync-logger.service.ts index 5b402376c4..989a3d60e9 100644 --- a/apps/api/src/integration-platform/services/integration-sync-logger.service.ts +++ b/apps/api/src/integration-platform/services/integration-sync-logger.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { db } from '@db'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; interface StartLogParams { connectionId: string; diff --git a/apps/api/src/integration-platform/services/oauth-credentials.service.ts b/apps/api/src/integration-platform/services/oauth-credentials.service.ts index 2c584f01fb..412051da13 100644 --- a/apps/api/src/integration-platform/services/oauth-credentials.service.ts +++ b/apps/api/src/integration-platform/services/oauth-credentials.service.ts @@ -6,7 +6,7 @@ import { EncryptedData, } from './credential-vault.service'; import { getManifest, type OAuthConfig } from '@trycompai/integration-platform'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; export interface OAuthCredentials { clientId: string; diff --git a/apps/api/src/org-chart/org-chart.service.ts b/apps/api/src/org-chart/org-chart.service.ts index 5cd5275c86..8e36d5d299 100644 --- a/apps/api/src/org-chart/org-chart.service.ts +++ b/apps/api/src/org-chart/org-chart.service.ts @@ -11,7 +11,7 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { s3Client, BUCKET_NAME } from '@/app/s3'; import type { UpsertOrgChartDto } from './dto/upsert-org-chart.dto'; import type { UploadOrgChartDto } from './dto/upload-org-chart.dto'; diff --git a/apps/api/src/organization/organization.service.ts b/apps/api/src/organization/organization.service.ts index 5be0bf2eac..f053e1ef75 100644 --- a/apps/api/src/organization/organization.service.ts +++ b/apps/api/src/organization/organization.service.ts @@ -9,7 +9,7 @@ import { import { allRoles } from '@trycompai/auth'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db, Role } from '@trycompai/db'; +import { db, Role } from '@db'; import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '../app/s3'; import type { UpdateOrganizationDto } from './dto/update-organization.dto'; import type { TransferOwnershipResponseDto } from './dto/transfer-ownership.dto'; diff --git a/apps/api/src/people/dto/create-people.dto.ts b/apps/api/src/people/dto/create-people.dto.ts index 0c272341d0..5149ae451d 100644 --- a/apps/api/src/people/dto/create-people.dto.ts +++ b/apps/api/src/people/dto/create-people.dto.ts @@ -7,7 +7,7 @@ import { IsBoolean, IsNumber, } from 'class-validator'; -import { Departments } from '@trycompai/db'; +import { Departments } from '@db'; export class CreatePeopleDto { @ApiProperty({ diff --git a/apps/api/src/people/dto/people-responses.dto.ts b/apps/api/src/people/dto/people-responses.dto.ts index 86aee2e23b..a87ab60e33 100644 --- a/apps/api/src/people/dto/people-responses.dto.ts +++ b/apps/api/src/people/dto/people-responses.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Departments } from '@trycompai/db'; +import { Departments } from '@db'; export class UserResponseDto { @ApiProperty({ diff --git a/apps/api/src/people/people-fleet.helper.ts b/apps/api/src/people/people-fleet.helper.ts index 3b8b7066ca..9e3e8d1bbd 100644 --- a/apps/api/src/people/people-fleet.helper.ts +++ b/apps/api/src/people/people-fleet.helper.ts @@ -1,5 +1,5 @@ import { Logger } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { FleetService } from '../lib/fleet.service'; const MDM_POLICY_ID = -9999; diff --git a/apps/api/src/people/people-invite.service.spec.ts b/apps/api/src/people/people-invite.service.spec.ts index 276ff511e3..8e9c5b3232 100644 --- a/apps/api/src/people/people-invite.service.spec.ts +++ b/apps/api/src/people/people-invite.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { PeopleInviteService } from './people-invite.service'; -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { organization: { findUnique: jest.fn(), @@ -33,7 +33,7 @@ jest.mock('../email/templates/invite-member', () => ({ InviteEmail: jest.fn().mockReturnValue('mocked-react-element'), })); -import { db } from '@trycompai/db'; +import { db } from '@db'; import { triggerEmail } from '../email/trigger-email'; const mockDb = db as jest.Mocked; diff --git a/apps/api/src/people/people-invite.service.ts b/apps/api/src/people/people-invite.service.ts index 0bfd014645..2a51e67f16 100644 --- a/apps/api/src/people/people-invite.service.ts +++ b/apps/api/src/people/people-invite.service.ts @@ -4,7 +4,7 @@ import { BadRequestException, ForbiddenException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { triggerEmail } from '../email/trigger-email'; import { InviteEmail } from '../email/templates/invite-member'; import type { InviteItemDto } from './dto/invite-people.dto'; diff --git a/apps/api/src/people/people.service.spec.ts b/apps/api/src/people/people.service.spec.ts index ba105ddb0f..a2f5c4e810 100644 --- a/apps/api/src/people/people.service.spec.ts +++ b/apps/api/src/people/people.service.spec.ts @@ -10,7 +10,7 @@ import { MemberValidator } from './utils/member-validator'; import { MemberQueries } from './utils/member-queries'; // Mock the database -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { member: { findFirst: jest.fn(), @@ -67,7 +67,7 @@ jest.mock('@trycompai/email', () => ({ jest.mock('./utils/member-validator'); jest.mock('./utils/member-queries'); -import { db } from '@trycompai/db'; +import { db } from '@db'; describe('PeopleService', () => { let service: PeopleService; diff --git a/apps/api/src/people/people.service.ts b/apps/api/src/people/people.service.ts index cbaf0a5dab..b6692373a4 100644 --- a/apps/api/src/people/people.service.ts +++ b/apps/api/src/people/people.service.ts @@ -5,7 +5,7 @@ import { BadRequestException, ForbiddenException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { FleetService } from '../lib/fleet.service'; import { BUILT_IN_ROLE_PERMISSIONS } from '@trycompai/auth'; import type { PeopleResponseDto } from './dto/people-responses.dto'; diff --git a/apps/api/src/people/utils/member-deactivation.ts b/apps/api/src/people/utils/member-deactivation.ts index 5965b5b86a..cb0c6cf862 100644 --- a/apps/api/src/people/utils/member-deactivation.ts +++ b/apps/api/src/people/utils/member-deactivation.ts @@ -1,4 +1,4 @@ -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { isUserUnsubscribed } from '@trycompai/email'; import { Logger } from '@nestjs/common'; import { triggerEmail } from '../../email/trigger-email'; diff --git a/apps/api/src/people/utils/member-queries.ts b/apps/api/src/people/utils/member-queries.ts index 97e4563122..09fc99c605 100644 --- a/apps/api/src/people/utils/member-queries.ts +++ b/apps/api/src/people/utils/member-queries.ts @@ -1,4 +1,4 @@ -import { db } from '@trycompai/db'; +import { db } from '@db'; import type { PeopleResponseDto } from '../dto/people-responses.dto'; import type { CreatePeopleDto } from '../dto/create-people.dto'; import type { UpdatePeopleDto } from '../dto/update-people.dto'; diff --git a/apps/api/src/people/utils/member-validator.ts b/apps/api/src/people/utils/member-validator.ts index 8236990aed..c71ee29340 100644 --- a/apps/api/src/people/utils/member-validator.ts +++ b/apps/api/src/people/utils/member-validator.ts @@ -1,5 +1,5 @@ import { NotFoundException, BadRequestException } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; export class MemberValidator { /** diff --git a/apps/api/src/policies/policies.controller.spec.ts b/apps/api/src/policies/policies.controller.spec.ts index e4f7653a6a..b58c0f97ff 100644 --- a/apps/api/src/policies/policies.controller.spec.ts +++ b/apps/api/src/policies/policies.controller.spec.ts @@ -21,7 +21,7 @@ jest.mock('@trycompai/auth', () => ({ BUILT_IN_ROLE_PERMISSIONS: {}, })); -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { policy: { findFirst: jest.fn(), @@ -291,7 +291,7 @@ describe('PoliciesController', () => { describe('getPolicyControls', () => { it('should return mapped and all controls', async () => { - const { db } = require('@trycompai/db'); + const { db } = require('@db'); const mappedControls = [ { id: 'ctrl_1', name: 'Control 1', description: 'desc' }, ]; @@ -317,7 +317,7 @@ describe('PoliciesController', () => { }); it('should return empty mappedControls when policy not found', async () => { - const { db } = require('@trycompai/db'); + const { db } = require('@db'); db.policy.findFirst.mockResolvedValue(null); db.control.findMany.mockResolvedValue([]); @@ -333,7 +333,7 @@ describe('PoliciesController', () => { describe('addPolicyControls', () => { it('should connect controls to policy and return success', async () => { - const { db } = require('@trycompai/db'); + const { db } = require('@db'); db.policy.update.mockResolvedValue({}); const result = await controller.addPolicyControls( @@ -357,7 +357,7 @@ describe('PoliciesController', () => { describe('removePolicyControl', () => { it('should disconnect control from policy and return success', async () => { - const { db } = require('@trycompai/db'); + const { db } = require('@db'); db.policy.update.mockResolvedValue({}); const result = await controller.removePolicyControl( diff --git a/apps/api/src/policies/policies.controller.ts b/apps/api/src/policies/policies.controller.ts index 02a43d63b0..6e8fbf66fc 100644 --- a/apps/api/src/policies/policies.controller.ts +++ b/apps/api/src/policies/policies.controller.ts @@ -30,7 +30,7 @@ import { import type { Response } from 'express'; import { openai } from '@ai-sdk/openai'; import { streamText, convertToModelMessages, type UIMessage } from 'ai'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { auth as triggerAuth, tasks } from '@trigger.dev/sdk'; import type { updatePolicy } from '../trigger/policies/update-policy'; import { AuditRead } from '../audit/skip-audit-log.decorator'; diff --git a/apps/api/src/policies/policies.service.ts b/apps/api/src/policies/policies.service.ts index ac44c16709..634b8a24b8 100644 --- a/apps/api/src/policies/policies.service.ts +++ b/apps/api/src/policies/policies.service.ts @@ -4,7 +4,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; -import { db, Frequency, PolicyStatus, Prisma } from '@trycompai/db'; +import { db, Frequency, PolicyStatus, Prisma } from '@db'; import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; import { AttachmentsService } from '../attachments/attachments.service'; import { PolicyPdfRendererService } from '../trust-portal/policy-pdf-renderer.service'; diff --git a/apps/api/src/risks/dto/create-risk.dto.ts b/apps/api/src/risks/dto/create-risk.dto.ts index 0875bc2cc2..874f7d4077 100644 --- a/apps/api/src/risks/dto/create-risk.dto.ts +++ b/apps/api/src/risks/dto/create-risk.dto.ts @@ -7,7 +7,7 @@ import { Likelihood, Impact, RiskTreatmentType, -} from '@trycompai/db'; +} from '@db'; export class CreateRiskDto { @ApiProperty({ diff --git a/apps/api/src/risks/dto/get-risks-query.dto.ts b/apps/api/src/risks/dto/get-risks-query.dto.ts index cd79dc4d1b..33c61b41ad 100644 --- a/apps/api/src/risks/dto/get-risks-query.dto.ts +++ b/apps/api/src/risks/dto/get-risks-query.dto.ts @@ -12,7 +12,7 @@ import { RiskCategory, Departments, RiskStatus, -} from '@trycompai/db'; +} from '@db'; export enum RiskSortBy { CREATED_AT = 'createdAt', diff --git a/apps/api/src/risks/dto/risk-response.dto.ts b/apps/api/src/risks/dto/risk-response.dto.ts index 36d1bd23e5..b5e647e291 100644 --- a/apps/api/src/risks/dto/risk-response.dto.ts +++ b/apps/api/src/risks/dto/risk-response.dto.ts @@ -6,7 +6,7 @@ import { Likelihood, Impact, RiskTreatmentType, -} from '@trycompai/db'; +} from '@db'; export class RiskResponseDto { @ApiProperty({ diff --git a/apps/api/src/risks/risks.controller.spec.ts b/apps/api/src/risks/risks.controller.spec.ts index 92120ee227..06e494133f 100644 --- a/apps/api/src/risks/risks.controller.spec.ts +++ b/apps/api/src/risks/risks.controller.spec.ts @@ -7,6 +7,12 @@ import { RisksController } from './risks.controller'; import { RisksService } from './risks.service'; // Mock auth.server to avoid importing better-auth ESM in Jest +jest.mock('@db', () => ({ + ...jest.requireActual('@prisma/client'), + db: {}, + Prisma: { PrismaClientKnownRequestError: class PrismaClientKnownRequestError extends Error { code: string; constructor(message: string, { code }: { code: string }) { super(message); this.code = code; } } }, +})); + jest.mock('../auth/auth.server', () => ({ auth: { api: { @@ -31,6 +37,7 @@ import { buildRiskAssignmentFilter, hasRiskAccess, } from '../utils/assignment-filter'; +import { RiskCategory } from '@db'; const mockBuildRiskAssignmentFilter = buildRiskAssignmentFilter as jest.MockedFunction< @@ -59,7 +66,7 @@ describe('RisksController', () => { const authContextNoUser: AuthContext = { organizationId: orgId, - authType: 'apikey', + authType: 'api-key', isApiKey: true, isPlatformAdmin: false, userRoles: ['admin'], @@ -119,7 +126,7 @@ describe('RisksController', () => { }; it('should call findAllByOrganization with correct parameters', async () => { - risksService.findAllByOrganization.mockResolvedValue(paginatedResult); + risksService.findAllByOrganization.mockResolvedValue(paginatedResult as unknown as Awaited>); const query = { page: 1, perPage: 10 }; await controller.getAllRisks(query, orgId, authContext); @@ -137,7 +144,7 @@ describe('RisksController', () => { }); it('should return paginated data with auth info', async () => { - risksService.findAllByOrganization.mockResolvedValue(paginatedResult); + risksService.findAllByOrganization.mockResolvedValue(paginatedResult as unknown as Awaited>); const result = await controller.getAllRisks({}, orgId, authContext); @@ -155,18 +162,18 @@ describe('RisksController', () => { }); it('should omit authenticatedUser when userId is not present', async () => { - risksService.findAllByOrganization.mockResolvedValue(paginatedResult); + risksService.findAllByOrganization.mockResolvedValue(paginatedResult as unknown as Awaited>); const result = await controller.getAllRisks({}, orgId, authContextNoUser); - expect(result.authType).toBe('apikey'); + expect(result.authType).toBe('api-key'); expect(result).not.toHaveProperty('authenticatedUser'); }); it('should pass assignment filter from buildRiskAssignmentFilter', async () => { const assignmentFilter = { assigneeId: 'mem_123' }; mockBuildRiskAssignmentFilter.mockReturnValue(assignmentFilter); - risksService.findAllByOrganization.mockResolvedValue(paginatedResult); + risksService.findAllByOrganization.mockResolvedValue(paginatedResult as unknown as Awaited>); await controller.getAllRisks({}, orgId, authContext); @@ -233,7 +240,7 @@ describe('RisksController', () => { ]; it('should call getStatsByDepartment with organizationId', async () => { - risksService.getStatsByDepartment.mockResolvedValue(deptStats); + risksService.getStatsByDepartment.mockResolvedValue(deptStats as unknown as Awaited>); await controller.getStatsByDepartment(orgId, authContext); @@ -241,7 +248,7 @@ describe('RisksController', () => { }); it('should return data with auth info', async () => { - risksService.getStatsByDepartment.mockResolvedValue(deptStats); + risksService.getStatsByDepartment.mockResolvedValue(deptStats as unknown as Awaited>); const result = await controller.getStatsByDepartment(orgId, authContext); @@ -258,7 +265,7 @@ describe('RisksController', () => { describe('getRiskById', () => { it('should call findById with correct parameters', async () => { - risksService.findById.mockResolvedValue(mockRisk); + risksService.findById.mockResolvedValue(mockRisk as unknown as Awaited>); await controller.getRiskById('risk_1', orgId, authContext); @@ -266,7 +273,7 @@ describe('RisksController', () => { }); it('should return risk with auth info', async () => { - risksService.findById.mockResolvedValue(mockRisk); + risksService.findById.mockResolvedValue(mockRisk as unknown as Awaited>); const result = await controller.getRiskById('risk_1', orgId, authContext); @@ -281,7 +288,7 @@ describe('RisksController', () => { }); it('should check hasRiskAccess and throw ForbiddenException if denied', async () => { - risksService.findById.mockResolvedValue(mockRisk); + risksService.findById.mockResolvedValue(mockRisk as unknown as Awaited>); mockHasRiskAccess.mockReturnValue(false); await expect( @@ -297,7 +304,7 @@ describe('RisksController', () => { }); it('should pass isApiKey option to hasRiskAccess', async () => { - risksService.findById.mockResolvedValue(mockRisk); + risksService.findById.mockResolvedValue(mockRisk as unknown as Awaited>); await controller.getRiskById('risk_1', orgId, authContextNoUser); @@ -314,10 +321,11 @@ describe('RisksController', () => { const createDto = { title: 'New Risk', description: 'Description', + category: RiskCategory.operational, }; it('should call create with organizationId and dto', async () => { - risksService.create.mockResolvedValue(mockRisk); + risksService.create.mockResolvedValue(mockRisk as unknown as Awaited>); await controller.createRisk(createDto, orgId, authContext); @@ -325,7 +333,7 @@ describe('RisksController', () => { }); it('should return created risk with auth info', async () => { - risksService.create.mockResolvedValue(mockRisk); + risksService.create.mockResolvedValue(mockRisk as unknown as Awaited>); const result = await controller.createRisk(createDto, orgId, authContext); @@ -340,7 +348,7 @@ describe('RisksController', () => { }); it('should omit authenticatedUser for API key auth', async () => { - risksService.create.mockResolvedValue(mockRisk); + risksService.create.mockResolvedValue(mockRisk as unknown as Awaited>); const result = await controller.createRisk( createDto, @@ -349,7 +357,7 @@ describe('RisksController', () => { ); expect(result).not.toHaveProperty('authenticatedUser'); - expect(result.authType).toBe('apikey'); + expect(result.authType).toBe('api-key'); }); }); @@ -358,7 +366,7 @@ describe('RisksController', () => { const updatedRisk = { ...mockRisk, title: 'Updated Risk' }; it('should call updateById with correct parameters', async () => { - risksService.updateById.mockResolvedValue(updatedRisk); + risksService.updateById.mockResolvedValue(updatedRisk as unknown as Awaited>); await controller.updateRisk('risk_1', updateDto, orgId, authContext); @@ -370,7 +378,7 @@ describe('RisksController', () => { }); it('should return updated risk with auth info', async () => { - risksService.updateById.mockResolvedValue(updatedRisk); + risksService.updateById.mockResolvedValue(updatedRisk as unknown as Awaited>); const result = await controller.updateRisk( 'risk_1', diff --git a/apps/api/src/risks/risks.service.ts b/apps/api/src/risks/risks.service.ts index cb049dbbe0..440c44d709 100644 --- a/apps/api/src/risks/risks.service.ts +++ b/apps/api/src/risks/risks.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db, Prisma } from '@trycompai/db'; +import { db, Prisma } from '@db'; import { CreateRiskDto } from './dto/create-risk.dto'; import { GetRisksQueryDto } from './dto/get-risks-query.dto'; import { UpdateRiskDto } from './dto/update-risk.dto'; diff --git a/apps/api/src/roles/roles.service.spec.ts b/apps/api/src/roles/roles.service.spec.ts index fe9165dd90..7b065efa0b 100644 --- a/apps/api/src/roles/roles.service.spec.ts +++ b/apps/api/src/roles/roles.service.spec.ts @@ -74,7 +74,7 @@ jest.mock('@trycompai/auth', () => { }); // Mock the database -jest.mock('@trycompai/db', () => ({ +jest.mock('@db', () => ({ db: { organizationRole: { findFirst: jest.fn(), @@ -90,7 +90,7 @@ jest.mock('@trycompai/db', () => ({ }, })); -import { db } from '@trycompai/db'; +import { db } from '@db'; describe('RolesService', () => { let service: RolesService; diff --git a/apps/api/src/roles/roles.service.ts b/apps/api/src/roles/roles.service.ts index 43919a5837..a00b5bb0c7 100644 --- a/apps/api/src/roles/roles.service.ts +++ b/apps/api/src/roles/roles.service.ts @@ -1,5 +1,5 @@ import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { statement, allRoles, BUILT_IN_ROLE_PERMISSIONS, BUILT_IN_ROLE_OBLIGATIONS, type RoleObligations } from '@trycompai/auth'; import type { CreateRoleDto } from './dto/create-role.dto'; import type { UpdateRoleDto } from './dto/update-role.dto'; diff --git a/apps/api/src/secrets/secrets.service.ts b/apps/api/src/secrets/secrets.service.ts index ececc86859..1ffb5330e5 100644 --- a/apps/api/src/secrets/secrets.service.ts +++ b/apps/api/src/secrets/secrets.service.ts @@ -4,7 +4,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { encrypt, decrypt, type EncryptedData } from './encryption.util'; @Injectable() diff --git a/apps/api/src/security-penetration-tests/pentest-billing.service.ts b/apps/api/src/security-penetration-tests/pentest-billing.service.ts index c07436e01c..b10073b209 100644 --- a/apps/api/src/security-penetration-tests/pentest-billing.service.ts +++ b/apps/api/src/security-penetration-tests/pentest-billing.service.ts @@ -7,7 +7,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { StripeService } from '../stripe/stripe.service'; @Injectable() diff --git a/apps/api/src/security-penetration-tests/security-penetration-tests.service.spec.ts b/apps/api/src/security-penetration-tests/security-penetration-tests.service.spec.ts index 3c2afb64dc..623f8ff0a5 100644 --- a/apps/api/src/security-penetration-tests/security-penetration-tests.service.spec.ts +++ b/apps/api/src/security-penetration-tests/security-penetration-tests.service.spec.ts @@ -1,5 +1,5 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { createHash } from 'node:crypto'; import type { CredentialVaultService } from '../integration-platform/services/credential-vault.service'; import type { CreatePenetrationTestDto } from './dto/create-penetration-test.dto'; @@ -9,7 +9,7 @@ const mockCredentialVaultService: jest.Mocked ({ +jest.mock('@db', () => ({ db: { securityPenetrationTestRun: { upsert: jest.fn(), @@ -737,7 +737,9 @@ describe('SecurityPenetrationTestsService', () => { it('throws when MACED API key is missing', async () => { process.env.MACED_API_KEY = ''; - const serviceWithoutKey = new SecurityPenetrationTestsService(); + const serviceWithoutKey = new SecurityPenetrationTestsService( + mockCredentialVaultService as unknown as CredentialVaultService, + ); await expect(serviceWithoutKey.listReports('org_123')).rejects.toThrow( 'Maced API key not configured on server', diff --git a/apps/api/src/security-penetration-tests/security-penetration-tests.service.ts b/apps/api/src/security-penetration-tests/security-penetration-tests.service.ts index b494a3e510..1cae0b2230 100644 --- a/apps/api/src/security-penetration-tests/security-penetration-tests.service.ts +++ b/apps/api/src/security-penetration-tests/security-penetration-tests.service.ts @@ -6,7 +6,7 @@ import { Injectable, Logger, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { createHash, timingSafeEqual } from 'node:crypto'; import { CredentialVaultService } from '../integration-platform/services/credential-vault.service'; diff --git a/apps/api/src/task-management/task-management.controller.ts b/apps/api/src/task-management/task-management.controller.ts index 78a8a9ac45..4118e03fc4 100644 --- a/apps/api/src/task-management/task-management.controller.ts +++ b/apps/api/src/task-management/task-management.controller.ts @@ -23,7 +23,7 @@ import { import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import { AuthContext, OrganizationId } from '../auth/auth-context.decorator'; import type { AuthContext as AuthContextType } from '../auth/types'; -import { TaskItemEntityType } from '@trycompai/db'; +import { TaskItemEntityType } from '@db'; import { PermissionGuard } from '../auth/permission.guard'; import { RequirePermission } from '../auth/require-permission.decorator'; import { TaskManagementService } from './task-management.service'; diff --git a/apps/api/src/tasks/attachments.service.ts b/apps/api/src/tasks/attachments.service.ts index b6933d6aa2..2f636e46b0 100644 --- a/apps/api/src/tasks/attachments.service.ts +++ b/apps/api/src/tasks/attachments.service.ts @@ -11,7 +11,7 @@ import { Injectable, InternalServerErrorException, } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { randomBytes } from 'crypto'; import { AttachmentResponseDto } from './dto/task-responses.dto'; import { UploadAttachmentDto } from './dto/upload-attachment.dto'; diff --git a/apps/api/src/tasks/automations/automations.service.ts b/apps/api/src/tasks/automations/automations.service.ts index cdfaa59baf..3afedd380f 100644 --- a/apps/api/src/tasks/automations/automations.service.ts +++ b/apps/api/src/tasks/automations/automations.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import { UpdateAutomationDto } from './dto/update-automation.dto'; @Injectable() diff --git a/apps/api/src/tasks/evidence-export/evidence-export.service.ts b/apps/api/src/tasks/evidence-export/evidence-export.service.ts index be5a298c37..90f4ee5785 100644 --- a/apps/api/src/tasks/evidence-export/evidence-export.service.ts +++ b/apps/api/src/tasks/evidence-export/evidence-export.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { db } from '@trycompai/db'; +import { db } from '@db'; import AdmZip from 'adm-zip'; import { format } from 'date-fns'; import { configure as configureStringify } from 'safe-stable-stringify'; diff --git a/apps/api/src/tasks/tasks.controller.spec.ts b/apps/api/src/tasks/tasks.controller.spec.ts index 3335cddf0e..7e3ecc305d 100644 --- a/apps/api/src/tasks/tasks.controller.spec.ts +++ b/apps/api/src/tasks/tasks.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import { TaskStatus } from '@trycompai/db'; +import { TaskStatus } from '@db'; import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import { PermissionGuard } from '../auth/permission.guard'; import type { AuthContext } from '../auth/types'; @@ -8,6 +8,12 @@ import { TasksController } from './tasks.controller'; import { TasksService } from './tasks.service'; import { AttachmentsService } from '../attachments/attachments.service'; +jest.mock('@db', () => ({ + ...jest.requireActual('@prisma/client'), + db: {}, + Prisma: { PrismaClientKnownRequestError: class PrismaClientKnownRequestError extends Error { code: string; constructor(message: string, { code }: { code: string }) { super(message); this.code = code; } } }, +})); + jest.mock('../auth/auth.server', () => ({ auth: { api: { getSession: jest.fn() } }, })); @@ -247,7 +253,7 @@ describe('TasksController', () => { ...authContext, userId: undefined as unknown as string, isApiKey: true, - authType: 'apiKey', + authType: 'api-key', }; mockTasksService.getApiKeyActorUserId.mockResolvedValue('usr_api'); mockTasksService.updateTasksStatus.mockResolvedValue({ @@ -608,7 +614,7 @@ describe('TasksController', () => { ...authContext, userId: undefined as unknown as string, isApiKey: true, - authType: 'apiKey', + authType: 'api-key', }; mockTasksService.getApiKeyActorUserId.mockResolvedValue('usr_api'); mockTasksService.updateTask.mockResolvedValue({ id: 'tsk_1' }); @@ -817,7 +823,7 @@ describe('TasksController', () => { const apiKeyAuth: AuthContext = { ...authContext, isApiKey: true, - authType: 'apiKey', + authType: 'api-key', }; mockTasksService.verifyTaskAccess.mockResolvedValue(undefined); @@ -832,7 +838,7 @@ describe('TasksController', () => { const apiKeyAuth: AuthContext = { ...authContext, isApiKey: true, - authType: 'apiKey', + authType: 'api-key', }; const uploadDto = { fileName: 'file.pdf', diff --git a/apps/api/src/tasks/tasks.service.ts b/apps/api/src/tasks/tasks.service.ts index 9c3abc942e..62aa692387 100644 --- a/apps/api/src/tasks/tasks.service.ts +++ b/apps/api/src/tasks/tasks.service.ts @@ -5,7 +5,7 @@ import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; -import { db, TaskStatus, Prisma, TaskFrequency, Departments } from '@trycompai/db'; +import { db, TaskStatus, Prisma, TaskFrequency, Departments } from '@db'; import { TaskResponseDto } from './dto/task-responses.dto'; import { TaskNotifierService } from './task-notifier.service'; diff --git a/apps/api/src/training/training-certificate-pdf.service.spec.ts b/apps/api/src/training/training-certificate-pdf.service.spec.ts index 09a7e12363..5e2688c47f 100644 --- a/apps/api/src/training/training-certificate-pdf.service.spec.ts +++ b/apps/api/src/training/training-certificate-pdf.service.spec.ts @@ -3,7 +3,7 @@ import { TrainingCertificatePdfService } from './training-certificate-pdf.servic // Mock fetch for logo download global.fetch = jest.fn().mockResolvedValue({ ok: false, -}) as jest.Mock; +}) as unknown as typeof fetch; describe('TrainingCertificatePdfService', () => { let service: TrainingCertificatePdfService; diff --git a/apps/api/src/trigger/policies/update-policy-helpers.ts b/apps/api/src/trigger/policies/update-policy-helpers.ts index babdac0734..dbf7c5595b 100644 --- a/apps/api/src/trigger/policies/update-policy-helpers.ts +++ b/apps/api/src/trigger/policies/update-policy-helpers.ts @@ -1,6 +1,6 @@ import { openai } from '@ai-sdk/openai'; -import { db } from '@trycompai/db'; -import type { FrameworkEditorFramework, FrameworkEditorPolicyTemplate, Policy, Prisma } from '@trycompai/db'; +import { db } from '@db'; +import type { FrameworkEditorFramework, FrameworkEditorPolicyTemplate, Policy, Prisma } from '@db'; import { logger } from '@trigger.dev/sdk'; import { generateObject, NoObjectGeneratedError } from 'ai'; import { z } from 'zod'; diff --git a/apps/api/src/trigger/policies/update-policy-prompts.ts b/apps/api/src/trigger/policies/update-policy-prompts.ts index f20aad3c0e..be848cdb30 100644 --- a/apps/api/src/trigger/policies/update-policy-prompts.ts +++ b/apps/api/src/trigger/policies/update-policy-prompts.ts @@ -1,4 +1,4 @@ -import type { FrameworkEditorFramework, FrameworkEditorPolicyTemplate } from '@trycompai/db'; +import type { FrameworkEditorFramework, FrameworkEditorPolicyTemplate } from '@db'; import { logger } from '@trigger.dev/sdk'; export const generatePrompt = ({ diff --git a/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts b/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts index 7fffbc9d91..05eb458b18 100644 --- a/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts +++ b/apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts @@ -8,7 +8,7 @@ import { type TaskItemEntityType, } from '@db'; import { openai } from '@ai-sdk/openai'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; import type { Task } from '@trigger.dev/sdk'; import { logger, queue, schemaTask } from '@trigger.dev/sdk'; import { generateObject } from 'ai'; diff --git a/apps/api/src/trust-portal/dto/compliance-resource.dto.ts b/apps/api/src/trust-portal/dto/compliance-resource.dto.ts index e31c4a0eec..23a8c301c1 100644 --- a/apps/api/src/trust-portal/dto/compliance-resource.dto.ts +++ b/apps/api/src/trust-portal/dto/compliance-resource.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsString } from 'class-validator'; -import { TrustFramework } from '@prisma/client'; +import { TrustFramework } from '@db'; export class ComplianceResourceBaseDto { @ApiProperty({ diff --git a/apps/api/src/trust-portal/trust-access.controller.ts b/apps/api/src/trust-portal/trust-access.controller.ts index 4b754c426c..33269cdee8 100644 --- a/apps/api/src/trust-portal/trust-access.controller.ts +++ b/apps/api/src/trust-portal/trust-access.controller.ts @@ -32,7 +32,7 @@ import { ReclaimAccessDto, RevokeGrantDto, } from './dto/trust-access.dto'; -import { TrustFramework } from '@prisma/client'; +import { TrustFramework } from '@db'; import { SignNdaDto } from './dto/nda.dto'; import { TrustAccessService } from './trust-access.service'; diff --git a/apps/api/src/trust-portal/trust-access.service.ts b/apps/api/src/trust-portal/trust-access.service.ts index 54f74ccfa9..d784c1b53d 100644 --- a/apps/api/src/trust-portal/trust-access.service.ts +++ b/apps/api/src/trust-portal/trust-access.service.ts @@ -20,7 +20,7 @@ import { PolicyPdfRendererService } from './policy-pdf-renderer.service'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '../app/s3'; -import { Prisma, TrustFramework } from '@prisma/client'; +import { Prisma, TrustFramework } from '@db'; import archiver from 'archiver'; import { PassThrough, Readable } from 'stream'; import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; diff --git a/apps/api/src/trust-portal/trust-portal.service.ts b/apps/api/src/trust-portal/trust-portal.service.ts index f6c1975c1d..f2ab6f6fcd 100644 --- a/apps/api/src/trust-portal/trust-portal.service.ts +++ b/apps/api/src/trust-portal/trust-portal.service.ts @@ -1,4 +1,4 @@ -import { Prisma, TrustFramework } from '@prisma/client'; +import { Prisma, TrustFramework } from '@db'; import { BadRequestException, Injectable, diff --git a/apps/api/src/utils/assignment-filter.ts b/apps/api/src/utils/assignment-filter.ts index 010f21a1f7..d954d95197 100644 --- a/apps/api/src/utils/assignment-filter.ts +++ b/apps/api/src/utils/assignment-filter.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client'; +import { Prisma } from '@db'; /** * Roles that require assignment-based filtering for resources diff --git a/apps/api/src/utils/compliance-filters.ts b/apps/api/src/utils/compliance-filters.ts index 9baab13345..8411ca621e 100644 --- a/apps/api/src/utils/compliance-filters.ts +++ b/apps/api/src/utils/compliance-filters.ts @@ -3,7 +3,7 @@ import { type RoleObligations, allRoles, } from '@trycompai/auth'; -import { db } from '@trycompai/db'; +import { db } from '@db'; /** * Check if any of the given role names have the compliance obligation, diff --git a/apps/api/src/utils/department-visibility.spec.ts b/apps/api/src/utils/department-visibility.spec.ts index a23fe63284..9f6fe23631 100644 --- a/apps/api/src/utils/department-visibility.spec.ts +++ b/apps/api/src/utils/department-visibility.spec.ts @@ -1,4 +1,4 @@ -import { Departments, PolicyVisibility } from '@prisma/client'; +import { Departments, PolicyVisibility } from '@db'; import { isPrivilegedRole, buildPolicyVisibilityFilter, diff --git a/apps/api/src/utils/department-visibility.ts b/apps/api/src/utils/department-visibility.ts index 73df3d4498..973fd2396d 100644 --- a/apps/api/src/utils/department-visibility.ts +++ b/apps/api/src/utils/department-visibility.ts @@ -1,4 +1,4 @@ -import { Departments, Prisma, PolicyVisibility } from '@prisma/client'; +import { Departments, Prisma, PolicyVisibility } from '@db'; /** * Roles that have full access without department visibility filtering diff --git a/apps/api/src/vendors/dto/create-vendor.dto.ts b/apps/api/src/vendors/dto/create-vendor.dto.ts index 13ca9dff8f..4e5813f5fe 100644 --- a/apps/api/src/vendors/dto/create-vendor.dto.ts +++ b/apps/api/src/vendors/dto/create-vendor.dto.ts @@ -13,7 +13,7 @@ import { VendorStatus, Likelihood, Impact, -} from '@trycompai/db'; +} from '@db'; export class CreateVendorDto { @ApiProperty({ diff --git a/apps/api/src/vendors/dto/update-vendor.dto.ts b/apps/api/src/vendors/dto/update-vendor.dto.ts index cea91edff5..1d2c87deb4 100644 --- a/apps/api/src/vendors/dto/update-vendor.dto.ts +++ b/apps/api/src/vendors/dto/update-vendor.dto.ts @@ -13,7 +13,7 @@ import { VendorStatus, Likelihood, Impact, -} from '@trycompai/db'; +} from '@db'; /** * DTO for PATCH /vendors/:id diff --git a/apps/api/src/vendors/dto/vendor-response.dto.ts b/apps/api/src/vendors/dto/vendor-response.dto.ts index 7ffcc83ae0..5e5d8aa472 100644 --- a/apps/api/src/vendors/dto/vendor-response.dto.ts +++ b/apps/api/src/vendors/dto/vendor-response.dto.ts @@ -4,7 +4,7 @@ import { VendorStatus, Likelihood, Impact, -} from '@trycompai/db'; +} from '@db'; export class VendorResponseDto { @ApiProperty({ diff --git a/apps/api/src/vendors/vendors.service.ts b/apps/api/src/vendors/vendors.service.ts index 755e445414..cb94bd8404 100644 --- a/apps/api/src/vendors/vendors.service.ts +++ b/apps/api/src/vendors/vendors.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, Injectable, NotFoundException, Logger } from '@nestjs/common'; -import { db, TaskItemPriority, TaskItemStatus } from '@trycompai/db'; +import { db, TaskItemPriority, TaskItemStatus } from '@db'; import { CreateVendorDto } from './dto/create-vendor.dto'; import { UpdateVendorDto } from './dto/update-vendor.dto'; import { tasks } from '@trigger.dev/sdk'; -import { Prisma } from '@prisma/client'; +import { Prisma } from '@db'; import type { TriggerVendorRiskAssessmentVendorDto } from './dto/trigger-vendor-risk-assessment.dto'; import { resolveTaskCreatorAndAssignee } from '../trigger/vendor/vendor-risk-assessment/assignee'; diff --git a/apps/api/test/maced-contract.e2e-spec.ts b/apps/api/test/maced-contract.e2e-spec.ts index dd158935d0..6cbb299582 100644 --- a/apps/api/test/maced-contract.e2e-spec.ts +++ b/apps/api/test/maced-contract.e2e-spec.ts @@ -39,15 +39,18 @@ describeIfEnabled('Maced provider contract canary (e2e)', () => { expect(Number.isNaN(Date.parse(run.updatedAt))).toBe(false); if (run.repoUrl) { - expect(() => new URL(run.repoUrl)).not.toThrow(); + const repoUrl = run.repoUrl; + expect(() => new URL(repoUrl)).not.toThrow(); } if (run.temporalUiUrl) { - expect(() => new URL(run.temporalUiUrl)).not.toThrow(); + const temporalUiUrl = run.temporalUiUrl; + expect(() => new URL(temporalUiUrl)).not.toThrow(); } if (run.webhookUrl) { - expect(() => new URL(run.webhookUrl)).not.toThrow(); + const webhookUrl = run.webhookUrl; + expect(() => new URL(webhookUrl)).not.toThrow(); } }; diff --git a/apps/api/trigger.config.ts b/apps/api/trigger.config.ts index 16db1ed34f..c1a40e5813 100644 --- a/apps/api/trigger.config.ts +++ b/apps/api/trigger.config.ts @@ -13,8 +13,8 @@ export default defineConfig({ build: { extensions: [ prismaExtension({ - version: '6.13.0', - dbPackageVersion: '^1.3.15', // Version of @trycompai/db package with compiled JS + version: '7.6.0', + dbPackageVersion: '^2.0.0', }), integrationPlatformExtension(), emailExtension(), diff --git a/apps/app/.gitignore b/apps/app/.gitignore index c0616d7c5f..a9e6967739 100644 --- a/apps/app/.gitignore +++ b/apps/app/.gitignore @@ -52,5 +52,6 @@ next-env.d.ts # Generated Prisma Client prisma/generated +src/generated/ # Copied schema from @trycompai/db package - always generate fresh prisma/schema.prisma diff --git a/apps/app/customPrismaExtension.ts b/apps/app/customPrismaExtension.ts index f48e0f2fcd..d77a5d88a2 100644 --- a/apps/app/customPrismaExtension.ts +++ b/apps/app/customPrismaExtension.ts @@ -33,8 +33,7 @@ export class PrismaExtension implements BuildExtension { constructor(private options: PrismaExtensionOptions) { this.moduleExternals = [ '@prisma/client', - '@prisma/engines', - '@trycompai/db', // Add the published package to externals + '@trycompai/db', ]; } @@ -121,7 +120,13 @@ export class PrismaExtension implements BuildExtension { ); await cp(schemaPath, schemaDestinationPath); - // Add prisma generate command to generate the client from the copied schema + // Patch the schema to use prisma-client-js (CJS-compatible, populates @prisma/client) + // The published schema uses prisma-client provider which generates .ts files — not suitable for Node.js runtime + commands.push( + `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema.prisma`, + ); + + // Add prisma generate command to generate the client from the patched schema commands.push( `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma`, ); @@ -190,6 +195,14 @@ export class PrismaExtension implements BuildExtension { await mkdir(schemaDir, { recursive: true }); await cp(schemaSourcePath, schemaDestinationPath); + // Patch schema to use prisma-client-js (default output → @prisma/client) + const { readFileSync, writeFileSync } = await import('node:fs'); + let schemaContent = readFileSync(schemaDestinationPath, 'utf8'); + schemaContent = schemaContent + .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') + .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); + writeFileSync(schemaDestinationPath, schemaContent); + const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js'); if (existsSync(clientEntryPoint) && !process.env.TRIGGER_PRISMA_FORCE_GENERATE) { diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index df53597e00..eb0e1a9242 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -1,4 +1,3 @@ -import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin'; import { withBotId } from 'botid/next/config'; import type { NextConfig } from 'next'; import path from 'path'; @@ -13,6 +12,7 @@ const config: NextConfig = { // Ensure Turbopack can import .md files as raw strings during dev turbopack: { root: workspaceRoot, + resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json'], rules: { '*.md': { loaders: ['raw-loader'], @@ -21,12 +21,7 @@ const config: NextConfig = { }, }, - webpack: (config, { isServer }) => { - if (isServer) { - // Very important, DO NOT REMOVE, it's needed for Prisma to work in the server bundle - config.plugins = [...config.plugins, new PrismaPlugin()]; - } - + webpack: (config) => { // Enable importing .md files as raw strings during webpack builds config.module = config.module || { rules: [] }; config.module.rules = config.module.rules || []; @@ -46,7 +41,6 @@ const config: NextConfig = { transpilePackages: [ '@trycompai/auth', '@trycompai/db', - '@prisma/client', '@trycompai/design-system', '@trycompai/ui', '@carbon/icons-react', diff --git a/apps/app/package.json b/apps/app/package.json index 0cd30b8d30..a3d4f14d18 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -35,9 +35,9 @@ "@novu/api": "^1.6.0", "@novu/nextjs": "^3.10.1", "@number-flow/react": "^0.5.9", - "@prisma/client": "6.18.0", - "@prisma/instrumentation": "6.18.0", - "@prisma/nextjs-monorepo-workaround-plugin": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", + "@prisma/instrumentation": "7.6.0", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -67,7 +67,7 @@ "@trigger.dev/sdk": "4.4.3", "@trycompai/auth": "workspace:*", "@trycompai/company": "workspace:*", - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "@trycompai/design-system": "^1.0.32", "@trycompai/email": "workspace:*", "@trycompai/integration-platform": "workspace:*", @@ -108,7 +108,7 @@ "playwright-core": "^1.52.0", "posthog-js": "^1.236.6", "posthog-node": "^5.8.2", - "prisma": "6.18.0", + "prisma": "7.6.0", "puppeteer-core": "^24.7.2", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -186,8 +186,8 @@ "scripts": { "analyze-locale-usage": "bunx tsx src/locales/analyze-locale-usage.ts", "build": "next build", - "build:docker": "prisma generate && next build", - "db:generate": "bun run db:getschema && prisma generate", + "build:docker": "prisma generate && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma && next build", + "db:generate": "bun run db:getschema && prisma generate && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/app", "deploy:trigger-prod": "npx trigger.dev@4.4.3 deploy", diff --git a/apps/app/prisma.config.ts b/apps/app/prisma.config.ts new file mode 100644 index 0000000000..f0e7629866 --- /dev/null +++ b/apps/app/prisma.config.ts @@ -0,0 +1,9 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index a696328bef..9abf612b4f 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -1,7 +1,13 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient } from '../src/generated/prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -export const db = globalForPrisma.prisma || new PrismaClient(); +function createPrismaClient(): PrismaClient { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const db = globalForPrisma.prisma || createPrismaClient(); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/apps/app/prisma/index.ts b/apps/app/prisma/index.ts index 54d1c4b9c9..ccdcea1dfa 100644 --- a/apps/app/prisma/index.ts +++ b/apps/app/prisma/index.ts @@ -1,2 +1 @@ -export * from '@prisma/client'; -export { db } from './client'; +export * from '../src/generated/prisma/browser'; diff --git a/apps/app/prisma/server.ts b/apps/app/prisma/server.ts new file mode 100644 index 0000000000..7969070f3c --- /dev/null +++ b/apps/app/prisma/server.ts @@ -0,0 +1,2 @@ +export * from '../src/generated/prisma/client'; +export { db } from './client'; diff --git a/apps/app/src/actions/files/upload-file.ts b/apps/app/src/actions/files/upload-file.ts index e1a0f5aa82..d45f570b17 100644 --- a/apps/app/src/actions/files/upload-file.ts +++ b/apps/app/src/actions/files/upload-file.ts @@ -5,7 +5,7 @@ import { auth } from '@/utils/auth'; import { logger } from '@/utils/logger'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { AttachmentEntityType, AttachmentType, db } from '@db'; +import { AttachmentEntityType, AttachmentType, db } from '@db/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; diff --git a/apps/app/src/actions/organization/accept-invitation.ts b/apps/app/src/actions/organization/accept-invitation.ts index 0570f9b8dd..ce7d7f98c7 100644 --- a/apps/app/src/actions/organization/accept-invitation.ts +++ b/apps/app/src/actions/organization/accept-invitation.ts @@ -2,7 +2,7 @@ import { createTrainingVideoEntries } from '@/lib/db/employee'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; diff --git a/apps/app/src/actions/organization/get-api-keys-action.ts b/apps/app/src/actions/organization/get-api-keys-action.ts index 85149c4e76..048bae396e 100644 --- a/apps/app/src/actions/organization/get-api-keys-action.ts +++ b/apps/app/src/actions/organization/get-api-keys-action.ts @@ -2,7 +2,7 @@ import type { ActionResponse } from '@/actions/types'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; export const getApiKeysAction = async (): Promise< diff --git a/apps/app/src/actions/organization/get-organization-users-action.ts b/apps/app/src/actions/organization/get-organization-users-action.ts index aaa9d2e496..022a496300 100644 --- a/apps/app/src/actions/organization/get-organization-users-action.ts +++ b/apps/app/src/actions/organization/get-organization-users-action.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { authActionClient } from '../safe-action'; interface User { diff --git a/apps/app/src/actions/organization/lib/get-framework-names.ts b/apps/app/src/actions/organization/lib/get-framework-names.ts index 5e6b8eab81..a14c713f05 100644 --- a/apps/app/src/actions/organization/lib/get-framework-names.ts +++ b/apps/app/src/actions/organization/lib/get-framework-names.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; /** * Fetch framework names by IDs and convert them to lowercase with no spaces diff --git a/apps/app/src/actions/organization/lib/initialize-organization.ts b/apps/app/src/actions/organization/lib/initialize-organization.ts index 7d897b771e..963c18b805 100644 --- a/apps/app/src/actions/organization/lib/initialize-organization.ts +++ b/apps/app/src/actions/organization/lib/initialize-organization.ts @@ -1,4 +1,4 @@ -import { db, Prisma } from '@db'; +import { db, Prisma } from '@db/server'; /** * Policy.content is Json[] (the inner nodes of a TipTap document), diff --git a/apps/app/src/actions/organization/remove-employee.ts b/apps/app/src/actions/organization/remove-employee.ts index 1e267426db..0f36d8ee17 100644 --- a/apps/app/src/actions/organization/remove-employee.ts +++ b/apps/app/src/actions/organization/remove-employee.ts @@ -1,7 +1,7 @@ 'use server'; import { serverApi } from '@/lib/api-server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/organization/update-organization-access-request-form-action.ts b/apps/app/src/actions/organization/update-organization-access-request-form-action.ts index adfd12e7a0..d8e8172d80 100644 --- a/apps/app/src/actions/organization/update-organization-access-request-form-action.ts +++ b/apps/app/src/actions/organization/update-organization-access-request-form-action.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/organization/update-organization-device-agent-step-action.ts b/apps/app/src/actions/organization/update-organization-device-agent-step-action.ts index aa2caf7585..3b837c77b3 100644 --- a/apps/app/src/actions/organization/update-organization-device-agent-step-action.ts +++ b/apps/app/src/actions/organization/update-organization-device-agent-step-action.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/organization/update-organization-evidence-approval-action.ts b/apps/app/src/actions/organization/update-organization-evidence-approval-action.ts index 06b43c01f7..d1c56a0f27 100644 --- a/apps/app/src/actions/organization/update-organization-evidence-approval-action.ts +++ b/apps/app/src/actions/organization/update-organization-evidence-approval-action.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/organization/update-organization-security-training-step-action.ts b/apps/app/src/actions/organization/update-organization-security-training-step-action.ts index 2f85c44249..5c2a4fac00 100644 --- a/apps/app/src/actions/organization/update-organization-security-training-step-action.ts +++ b/apps/app/src/actions/organization/update-organization-security-training-step-action.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/organization/update-organization-whistleblower-report-action.ts b/apps/app/src/actions/organization/update-organization-whistleblower-report-action.ts index e01cceb2c2..f28d41baf1 100644 --- a/apps/app/src/actions/organization/update-organization-whistleblower-report-action.ts +++ b/apps/app/src/actions/organization/update-organization-whistleblower-report-action.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/people/create-employee-action.ts b/apps/app/src/actions/people/create-employee-action.ts index 2717cfdef7..234a4709c6 100644 --- a/apps/app/src/actions/people/create-employee-action.ts +++ b/apps/app/src/actions/people/create-employee-action.ts @@ -1,7 +1,7 @@ 'use server'; import { completeEmployeeCreation } from '@/lib/db/employee'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { Prisma } from '@db/server'; import { authActionClient } from '../safe-action'; import { createEmployeeSchema } from '../schema'; import type { ActionResponse } from '../types'; @@ -42,7 +42,7 @@ export const createEmployeeAction = authActionClient } catch (error) { console.error('Error creating employee:', error); - if (error instanceof PrismaClientKnownRequestError && error.code === 'P2002') { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { return { success: false, error: 'An employee with this email already exists in your organization', diff --git a/apps/app/src/actions/policies/accept-requested-policy-changes.ts b/apps/app/src/actions/policies/accept-requested-policy-changes.ts index 6f8822f153..1d7a7c4604 100644 --- a/apps/app/src/actions/policies/accept-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/accept-requested-policy-changes.ts @@ -1,7 +1,7 @@ 'use server'; import { sendNewPolicyEmail } from '@/trigger/tasks/email/new-policy-email'; -import { db, PolicyStatus, type Prisma } from '@db'; +import { db, PolicyStatus, type Prisma } from '@db/server'; import { tasks } from '@trigger.dev/sdk'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; diff --git a/apps/app/src/actions/policies/update-version-content.ts b/apps/app/src/actions/policies/update-version-content.ts index eb43a6973b..9f922eaa30 100644 --- a/apps/app/src/actions/policies/update-version-content.ts +++ b/apps/app/src/actions/policies/update-version-content.ts @@ -2,7 +2,7 @@ import { revalidatePath } from 'next/cache'; import { z } from 'zod'; -import { db, PolicyStatus } from '@db'; +import { db, PolicyStatus } from '@db/server'; import type { Prisma } from '@db'; import { authActionClient } from '../safe-action'; diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index a091c798e3..7a86992263 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -3,7 +3,7 @@ import { env } from '@/env.mjs'; import { auth } from '@/utils/auth'; import { logger } from '@/utils/logger'; import { client } from '@trycompai/kv'; -import { AuditLogEntityType, db } from '@db'; +import { AuditLogEntityType, db } from '@db/server'; import { Ratelimit } from '@upstash/ratelimit'; import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from 'next-safe-action'; import { revalidatePath } from 'next/cache'; diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts index 4e83c04c2f..d7cf77538a 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts @@ -2,8 +2,7 @@ import { encrypt } from '@/lib/encryption'; import { getIntegrationHandler } from '@trycompai/integrations'; -import { db } from '@db'; -import { Prisma } from '@prisma/client'; +import { db, Prisma } from '@db/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts b/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts index 52ca2c9a38..8b830c54cd 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/data/getAllFrameworkInstancesWithControls.ts @@ -1,7 +1,7 @@ 'use server'; import type { Control, PolicyStatus, RequirementMap } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; export async function getAllFrameworkInstancesWithControls({ diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPeople.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPeople.ts index 04e96de395..22e0e935ed 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPeople.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPeople.ts @@ -1,6 +1,6 @@ import { filterComplianceMembers } from '@/lib/compliance'; import { trainingVideos } from '@/lib/data/training-videos'; -import { db } from '@db'; +import { db } from '@db/server'; export async function getPeopleScore(organizationId: string) { // Get all active members (employees and contractors); exclude inactive/deactivated diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts index eede592be7..3314565b3c 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; export async function getPublishedPoliciesScore(organizationId: string) { const allPolicies = await db.policy.findMany({ diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts index a0b76dd4b6..baae494049 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { cache } from 'react'; import { countStrictlyCompletedTasks, isTaskStrictlyComplete } from './taskEvidenceDocumentsScore'; diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/taskEvidenceDocumentsScore.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/taskEvidenceDocumentsScore.ts index 09f89e9e90..8c88677964 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/taskEvidenceDocumentsScore.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/taskEvidenceDocumentsScore.ts @@ -3,7 +3,7 @@ import { meetingSubTypeValues, toDbEvidenceFormType, } from '@trycompai/company'; -import { db } from '@db'; +import { db } from '@db/server'; const SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1000; diff --git a/apps/app/src/app/(app)/[orgId]/layout.tsx b/apps/app/src/app/(app)/[orgId]/layout.tsx index 3dcff20d3f..1a3aa8daed 100644 --- a/apps/app/src/app/(app)/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/layout.tsx @@ -8,7 +8,7 @@ import type { OrganizationFromMe } from '@/types'; import { auth } from '@/utils/auth'; import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db, Role } from '@db'; +import { db, Role } from '@db/server'; import dynamic from 'next/dynamic'; import { cookies, headers } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts index ee659a265e..853ff2a17f 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts @@ -3,7 +3,7 @@ import { authActionClient } from '@/actions/safe-action'; import { removeMemberFromOrgChart } from '@/lib/org-chart'; import type { Departments } from '@db'; -import { db, Prisma } from '@db'; +import { db, Prisma } from '@db/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; import { appErrors } from '../types'; diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx index 68deb59bec..788d2655d1 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx @@ -6,7 +6,7 @@ import { } from '@/lib/data/training-videos'; import { getFleetInstance } from '@/lib/fleet'; import type { EmployeeTrainingVideoCompletion, Member, User } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import type { Metadata } from 'next'; import { headers } from 'next/headers'; import { notFound, redirect } from 'next/navigation'; diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/checkMemberStatus.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/checkMemberStatus.ts index a946924829..d909791ec2 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/checkMemberStatus.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/checkMemberStatus.ts @@ -1,7 +1,7 @@ 'use server'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; export const checkMemberStatus = async ({ diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/inviteNewMember.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/inviteNewMember.ts index 30c12210a4..ad678b7118 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/inviteNewMember.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/inviteNewMember.ts @@ -3,7 +3,7 @@ import { maskEmailForLogs } from '@/lib/mask-email'; import { auth } from '@/utils/auth'; import type { Role } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; export const inviteNewMember = async ({ diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/reactivateMember.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/reactivateMember.ts index b880c97949..88d6b793a1 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/reactivateMember.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/reactivateMember.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '@/actions/safe-action'; diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts index e923ce80c3..c6c47bbbe8 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; // Remove unused Role import if not needed elsewhere // import { Role } from "@trycompai/db/types"; import { revalidatePath, revalidateTag } from 'next/cache'; diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx index f02fda7537..a607f992ff 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx @@ -2,7 +2,7 @@ import { filterComplianceMembers } from '@/lib/compliance'; import { trainingVideos as trainingVideosData } from '@/lib/data/training-videos'; import { serverApi } from '@/lib/server-api-client'; import type { Invitation, Member, User } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import { getEmployeeSyncConnections } from '../data/queries'; import { TeamMembersClient } from './TeamMembersClient'; 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 6d65e768c3..6149af8ecd 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 @@ -2,7 +2,7 @@ import { filterComplianceMembers } from '@/lib/compliance'; import { trainingVideos as trainingVideosData } from '@/lib/data/training-videos'; import { auth } from '@/utils/auth'; import type { Member, Organization, Policy, User } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { EmployeeCompletionChart } from './EmployeeCompletionChart'; diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts index 01277d8f5a..3bb1db1aa7 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts @@ -2,7 +2,7 @@ import { getFleetInstance } from '@/lib/fleet'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import type { CheckDetails, DeviceWithChecks } from '../types'; import type { Host } from '../types'; diff --git a/apps/app/src/app/(app)/[orgId]/people/page.tsx b/apps/app/src/app/(app)/[orgId]/people/page.tsx index 75f12ca4d5..6a2fcb9cf5 100644 --- a/apps/app/src/app/(app)/[orgId]/people/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/page.tsx @@ -3,7 +3,7 @@ import { auth } from '@/utils/auth'; import { s3Client, BUCKET_NAME } from '@/app/s3'; import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db } from '@db'; +import { db } from '@db/server'; import type { Metadata } from 'next'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/delete-policy-pdf.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/delete-policy-pdf.ts index 6fbd681ac2..fe256e6fac 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/delete-policy-pdf.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/delete-policy-pdf.ts @@ -3,7 +3,7 @@ import { authActionClient } from '@/actions/safe-action'; import { BUCKET_NAME, s3Client } from '@/app/s3'; import { DeleteObjectCommand } from '@aws-sdk/client-s3'; -import { db, PolicyDisplayFormat } from '@db'; +import { db, PolicyDisplayFormat } from '@db/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/get-policy-pdf-url.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/get-policy-pdf-url.ts index b2bef8b277..18842b8037 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/get-policy-pdf-url.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/get-policy-pdf-url.ts @@ -4,7 +4,7 @@ import { authActionClient } from '@/actions/safe-action'; import { BUCKET_NAME, s3Client } from '@/app/s3'; import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db } from '@db'; +import { db } from '@db/server'; import { z } from 'zod'; export const getPolicyPdfUrlAction = authActionClient diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/upload-policy-pdf.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/upload-policy-pdf.ts index fd22051678..845b599ef4 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/upload-policy-pdf.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/actions/upload-policy-pdf.ts @@ -3,7 +3,7 @@ import { authActionClient } from '@/actions/safe-action'; import { BUCKET_NAME, s3Client } from '@/app/s3'; import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; -import { db, PolicyDisplayFormat } from '@db'; +import { db, PolicyDisplayFormat } from '@db/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/get-policy-details.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/get-policy-details.ts index 4069151238..48ba6ec4a8 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/get-policy-details.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/get-policy-details.ts @@ -2,7 +2,7 @@ import { authActionClient } from '@/actions/safe-action'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { appErrors, policyDetailsInputSchema } from '../types'; diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/update-policy.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/update-policy.ts index 9a98495120..73a0af1796 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/update-policy.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/actions/update-policy.ts @@ -3,7 +3,7 @@ import { authActionClient } from '@/actions/safe-action'; import type { ActionResponse } from '@/actions/types'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { appErrors, updatePolicySchema } from '../types'; diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/page.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/page.tsx index daf8be58dc..5bb7573d7b 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/page.tsx @@ -1,5 +1,5 @@ import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { PageHeader, PageLayout } from '@trycompai/design-system'; import type { Metadata } from 'next'; import { headers } from 'next/headers'; diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/page.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/page.tsx index c51f4ad16a..23e72923b2 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/page.tsx @@ -1,5 +1,5 @@ import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import type { Metadata } from 'next'; import { redirect } from 'next/navigation'; diff --git a/apps/app/src/app/(app)/[orgId]/settings/page.tsx b/apps/app/src/app/(app)/[orgId]/settings/page.tsx index 3e80137595..c02b52c047 100644 --- a/apps/app/src/app/(app)/[orgId]/settings/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/settings/page.tsx @@ -4,7 +4,7 @@ import { UpdateOrganizationLogo } from '@/components/forms/organization/update-o import { UpdateOrganizationName } from '@/components/forms/organization/update-organization-name'; import { UpdateOrganizationWebsite } from '@/components/forms/organization/update-organization-website'; import { serverApi } from '@/lib/api-server'; -import { db } from '@db'; +import { db } from '@db/server'; import type { Metadata } from 'next'; export default async function OrganizationSettings({ diff --git a/apps/app/src/app/(app)/[orgId]/settings/portal/page.tsx b/apps/app/src/app/(app)/[orgId]/settings/portal/page.tsx index 2bdb82f01b..a87876f0cf 100644 --- a/apps/app/src/app/(app)/[orgId]/settings/portal/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/settings/portal/page.tsx @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import type { Metadata } from 'next'; import { PortalSettings } from './portal-settings'; diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts index 4002bc5a12..ec874f279d 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts @@ -1,7 +1,7 @@ 'use server'; import { groq } from '@ai-sdk/groq'; -import { db } from '@db'; +import { db } from '@db/server'; import { generateObject, NoObjectGeneratedError } from 'ai'; import { z } from 'zod'; import { diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx index b4a1785995..7c47ea597d 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { ArrowLeft, Lock } from 'lucide-react'; import Link from 'next/link'; import { redirect } from 'next/navigation'; diff --git a/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts index 88f75d90da..8f1a0e3583 100644 --- a/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts +++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts @@ -2,7 +2,7 @@ import { authActionClient } from '@/actions/safe-action'; import { env } from '@/env.mjs'; -import { db } from '@db'; +import { db } from '@db/server'; import { Vercel } from '@vercel/sdk'; import * as dns from 'node:dns'; import { revalidatePath, revalidateTag } from 'next/cache'; diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts index 19c68ce992..0c40552a67 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts +++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts @@ -3,7 +3,7 @@ 'use server'; import { authActionClient } from '@/actions/safe-action'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { updateVendorSchema } from './schema'; diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx index c848ec4a31..4713ba5d35 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx +++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx @@ -16,7 +16,7 @@ import { VendorInherentRiskChart } from './VendorInherentRiskChart'; import { VendorResidualRiskChart } from './VendorResidualRiskChart'; import type { Member, User, Vendor } from '@db'; import { CommentEntityType } from '@db'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; import { useRealtimeRun } from '@trigger.dev/react-hooks'; import { Breadcrumb, diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx index 2f1387970f..c03938582c 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx +++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx @@ -11,7 +11,7 @@ import { filterCertifications } from '@/components/vendor-risk-assessment/filter import { parseVendorRiskAssessmentDescription } from '@/components/vendor-risk-assessment/parse-vendor-risk-assessment-description'; import type { VendorRiskAssessmentCertification } from '@/components/vendor-risk-assessment/vendor-risk-assessment-types'; import { cn } from '@/lib/utils'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from '@db'; import { Button } from '@trycompai/design-system'; import { Launch } from '@trycompai/design-system/icons'; import Link from 'next/link'; diff --git a/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-category.tsx b/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-category.tsx index acf8d81776..26da6a87bb 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-category.tsx +++ b/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-category.tsx @@ -1,5 +1,5 @@ import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; -import { db, VendorCategory } from '@db'; +import { db, VendorCategory } from '@db/server'; import { VendorCategoryChart } from './category-chart'; const VENDOR_CATEGORIES = Object.values(VendorCategory); diff --git a/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-status.tsx b/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-status.tsx index ee47b55832..05520d72c6 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-status.tsx +++ b/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/components/charts/vendors-by-status.tsx @@ -1,5 +1,5 @@ import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; -import { db } from '@db'; +import { db } from '@db/server'; import { StatusChart } from './status-chart'; interface Props { organizationId: string; diff --git a/apps/app/src/app/(app)/invite/[code]/page.tsx b/apps/app/src/app/(app)/invite/[code]/page.tsx index f6e7891ff9..c0944d3a71 100644 --- a/apps/app/src/app/(app)/invite/[code]/page.tsx +++ b/apps/app/src/app/(app)/invite/[code]/page.tsx @@ -1,6 +1,6 @@ import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { AcceptInvite } from '../../setup/components/accept-invite'; diff --git a/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx b/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx index 933844d082..bf9588ca2c 100644 --- a/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx +++ b/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx @@ -1,5 +1,5 @@ import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { notFound, redirect } from 'next/navigation'; import { PostPaymentOnboarding } from '../components/PostPaymentOnboarding'; diff --git a/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts b/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts index 3e0e138410..7c58e1d8f9 100644 --- a/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts +++ b/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts @@ -5,7 +5,7 @@ import { steps } from '@/app/(app)/setup/lib/constants'; import { createFleetLabelForOrg } from '@/trigger/tasks/device/create-fleet-label-for-org'; import { onboardOrganization as onboardOrganizationTask } from '@/trigger/tasks/onboarding/onboard-organization'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { tasks } from '@trigger.dev/sdk'; import { revalidatePath } from 'next/cache'; import { cookies, headers } from 'next/headers'; diff --git a/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts b/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts index 3d82a37899..5ea1e258bb 100644 --- a/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts +++ b/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts @@ -5,7 +5,7 @@ import { authActionClientWithoutOrg } from '@/actions/safe-action'; import { env } from '@/env.mjs'; import { createTrainingVideoEntries } from '@/lib/db/employee'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; diff --git a/apps/app/src/app/(app)/setup/actions/create-organization.ts b/apps/app/src/app/(app)/setup/actions/create-organization.ts index 0fc8d689c5..e0adedae9a 100644 --- a/apps/app/src/app/(app)/setup/actions/create-organization.ts +++ b/apps/app/src/app/(app)/setup/actions/create-organization.ts @@ -6,7 +6,7 @@ import { createTrainingVideoEntries } from '@/lib/db/employee'; import { createFleetLabelForOrg } from '@/trigger/tasks/device/create-fleet-label-for-org'; import { onboardOrganization as onboardOrganizationTask } from '@/trigger/tasks/onboarding/onboard-organization'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { tasks } from '@trigger.dev/sdk'; import { revalidatePath } from 'next/cache'; import { cookies, headers } from 'next/headers'; diff --git a/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx b/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx index 1b2a7e4cc8..ff50a68b59 100644 --- a/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx +++ b/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx @@ -1,7 +1,7 @@ import { extractDomain, isDomainActiveStripeCustomer, isPublicEmailDomain } from '@/lib/stripe'; import { auth } from '@/utils/auth'; import { env } from '@/env.mjs'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { BookingStep } from './components/booking-step'; diff --git a/apps/app/src/app/api/auth/test-db/route.ts b/apps/app/src/app/api/auth/test-db/route.ts index 94103a5cb8..8e48ac475f 100644 --- a/apps/app/src/app/api/auth/test-db/route.ts +++ b/apps/app/src/app/api/auth/test-db/route.ts @@ -1,4 +1,4 @@ -import { db, Departments } from '@db'; +import { db, Departments } from '@db/server'; import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; diff --git a/apps/app/src/app/api/auth/test-grant-access/route.ts b/apps/app/src/app/api/auth/test-grant-access/route.ts index ea47ddcc49..982e3eab28 100644 --- a/apps/app/src/app/api/auth/test-grant-access/route.ts +++ b/apps/app/src/app/api/auth/test-grant-access/route.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { NextRequest, NextResponse } from 'next/server'; // Force dynamic rendering for this route diff --git a/apps/app/src/app/api/auth/test-login/route.ts b/apps/app/src/app/api/auth/test-login/route.ts index 47c98ba898..b494bb4507 100644 --- a/apps/app/src/app/api/auth/test-login/route.ts +++ b/apps/app/src/app/api/auth/test-login/route.ts @@ -1,5 +1,5 @@ import { auth } from '@/utils/auth'; -import { db, Departments } from '@db'; +import { db, Departments } from '@db/server'; import { NextRequest, NextResponse } from 'next/server'; // Force dynamic rendering for this route diff --git a/apps/app/src/app/api/email-preferences/route.ts b/apps/app/src/app/api/email-preferences/route.ts index b49d150b6e..e3505e8667 100644 --- a/apps/app/src/app/api/email-preferences/route.ts +++ b/apps/app/src/app/api/email-preferences/route.ts @@ -1,4 +1,4 @@ -import { db } from "@db"; +import { db } from "@db/server"; import { verifyUnsubscribeToken } from "@/lib/unsubscribe"; import { NextResponse } from "next/server"; import { z } from "zod"; diff --git a/apps/app/src/app/api/get-image-url/route.ts b/apps/app/src/app/api/get-image-url/route.ts index 100acc12b4..525ce74740 100644 --- a/apps/app/src/app/api/get-image-url/route.ts +++ b/apps/app/src/app/api/get-image-url/route.ts @@ -2,7 +2,7 @@ import { auth } from '@/utils/auth'; import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/app/s3'; import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db } from '@db'; +import { db } from '@db/server'; import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { diff --git a/apps/app/src/app/api/health/route.ts b/apps/app/src/app/api/health/route.ts index de41348426..34ab8bdbd1 100644 --- a/apps/app/src/app/api/health/route.ts +++ b/apps/app/src/app/api/health/route.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; diff --git a/apps/app/src/app/api/invitations/[id]/route.test.ts b/apps/app/src/app/api/invitations/[id]/route.test.ts index 11d40e0b0d..5bdfb934de 100644 --- a/apps/app/src/app/api/invitations/[id]/route.test.ts +++ b/apps/app/src/app/api/invitations/[id]/route.test.ts @@ -11,7 +11,7 @@ vi.mock('@/utils/auth', () => ({ })); // Mock db -vi.mock('@db', () => ({ +vi.mock('@db/server', () => ({ db: { member: { findFirst: vi.fn() }, invitation: { findFirst: vi.fn(), delete: vi.fn() }, @@ -21,7 +21,7 @@ vi.mock('@db', () => ({ // Import after mocks are declared import { DELETE } from './route'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; const mockGetSession = vi.mocked(auth.api.getSession); const mockMemberFindFirst = vi.mocked((db as any).member.findFirst); diff --git a/apps/app/src/app/api/policies/[policyId]/chat/route.ts b/apps/app/src/app/api/policies/[policyId]/chat/route.ts index a2f293b0b0..97798e22b3 100644 --- a/apps/app/src/app/api/policies/[policyId]/chat/route.ts +++ b/apps/app/src/app/api/policies/[policyId]/chat/route.ts @@ -1,6 +1,6 @@ import { auth } from '@/utils/auth'; import { anthropic } from '@ai-sdk/anthropic'; -import { db } from '@db'; +import { db } from '@db/server'; import { convertToModelMessages, streamText, stepCountIs, type UIMessage } from 'ai'; import { headers } from 'next/headers'; import { NextResponse } from 'next/server'; diff --git a/apps/app/src/app/api/qa/approve-org/route.ts b/apps/app/src/app/api/qa/approve-org/route.ts index e88329db5e..734d090950 100644 --- a/apps/app/src/app/api/qa/approve-org/route.ts +++ b/apps/app/src/app/api/qa/approve-org/route.ts @@ -1,5 +1,5 @@ import { timingSafeEqual } from 'crypto'; -import { db } from '@db'; +import { db } from '@db/server'; import { type NextRequest, NextResponse } from 'next/server'; /** diff --git a/apps/app/src/app/api/qa/delete-user/route.ts b/apps/app/src/app/api/qa/delete-user/route.ts index 818b979ef5..15281ebe72 100644 --- a/apps/app/src/app/api/qa/delete-user/route.ts +++ b/apps/app/src/app/api/qa/delete-user/route.ts @@ -1,5 +1,5 @@ import { timingSafeEqual } from 'crypto'; -import { db } from '@db'; +import { db } from '@db/server'; import { type NextRequest, NextResponse } from 'next/server'; /** diff --git a/apps/app/src/app/api/retool/reset-org/route.ts b/apps/app/src/app/api/retool/reset-org/route.ts index f30d1c6592..9430bc3c82 100644 --- a/apps/app/src/app/api/retool/reset-org/route.ts +++ b/apps/app/src/app/api/retool/reset-org/route.ts @@ -1,6 +1,6 @@ import { timingSafeEqual } from 'crypto'; import { initializeOrganization } from '@/actions/organization/lib/initialize-organization'; -import { db } from '@db'; +import { db } from '@db/server'; import { type NextRequest, NextResponse } from 'next/server'; // Configure this route to use Node.js runtime instead of Edge diff --git a/apps/app/src/app/api/training/certificate/route.ts b/apps/app/src/app/api/training/certificate/route.ts index 42c0a7005a..e2e505345c 100644 --- a/apps/app/src/app/api/training/certificate/route.ts +++ b/apps/app/src/app/api/training/certificate/route.ts @@ -1,5 +1,5 @@ import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { diff --git a/apps/app/src/app/api/user-frameworks/route.ts b/apps/app/src/app/api/user-frameworks/route.ts index 766a7865de..40ce62db8b 100644 --- a/apps/app/src/app/api/user-frameworks/route.ts +++ b/apps/app/src/app/api/user-frameworks/route.ts @@ -1,5 +1,5 @@ import { timingSafeEqual } from 'crypto'; -import { db } from '@db'; +import { db } from '@db/server'; import { NextResponse } from 'next/server'; export async function GET(request: Request) { diff --git a/apps/app/src/app/api/webhooks/stripe-pentest/route.ts b/apps/app/src/app/api/webhooks/stripe-pentest/route.ts index 2cf91a0099..beee6c1c2d 100644 --- a/apps/app/src/app/api/webhooks/stripe-pentest/route.ts +++ b/apps/app/src/app/api/webhooks/stripe-pentest/route.ts @@ -1,6 +1,6 @@ import { env } from '@/env.mjs'; import { stripe } from '@/lib/stripe'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { NextResponse } from 'next/server'; import type Stripe from 'stripe'; diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index 99af539a98..c738987486 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -1,7 +1,7 @@ import { serverApi } from '@/lib/api-server'; import { getDefaultRoute, mergePermissions, resolveBuiltInPermissions } from '@/lib/permissions'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/apps/app/src/app/unsubscribe/page.tsx b/apps/app/src/app/unsubscribe/page.tsx index 60a016eecb..7af11c670b 100644 --- a/apps/app/src/app/unsubscribe/page.tsx +++ b/apps/app/src/app/unsubscribe/page.tsx @@ -1,5 +1,5 @@ import { getUnsubscribeUrl } from '@/lib/unsubscribe'; -import { db } from '@db'; +import { db } from '@db/server'; import { redirect } from 'next/navigation'; interface PageProps { diff --git a/apps/app/src/app/unsubscribe/preferences/page.tsx b/apps/app/src/app/unsubscribe/preferences/page.tsx index 9ee954e796..e13ade7762 100644 --- a/apps/app/src/app/unsubscribe/preferences/page.tsx +++ b/apps/app/src/app/unsubscribe/preferences/page.tsx @@ -1,5 +1,5 @@ import { verifyUnsubscribeToken } from '@/lib/unsubscribe'; -import { db } from '@db'; +import { db } from '@db/server'; import { UnsubscribePreferencesClient, type EmailPreferences } from './client'; interface PageProps { diff --git a/apps/app/src/components/policies/charts/policies-by-assignee.tsx b/apps/app/src/components/policies/charts/policies-by-assignee.tsx index 31ccc426a4..181fc1b85b 100644 --- a/apps/app/src/components/policies/charts/policies-by-assignee.tsx +++ b/apps/app/src/components/policies/charts/policies-by-assignee.tsx @@ -1,5 +1,5 @@ import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; -import { db, PolicyStatus } from '@db'; +import { db, PolicyStatus } from '@db/server'; import type { CSSProperties } from 'react'; interface Props { diff --git a/apps/app/src/components/risks/charts/risks-by-status.tsx b/apps/app/src/components/risks/charts/risks-by-status.tsx index 139615ec0c..c1d0564aae 100644 --- a/apps/app/src/components/risks/charts/risks-by-status.tsx +++ b/apps/app/src/components/risks/charts/risks-by-status.tsx @@ -1,6 +1,6 @@ import { auth } from '@/utils/auth'; import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { cache } from 'react'; import { StatusChart } from './status-chart'; diff --git a/apps/app/src/components/sidebar.tsx b/apps/app/src/components/sidebar.tsx index 6d3eee370d..fe661f3117 100644 --- a/apps/app/src/components/sidebar.tsx +++ b/apps/app/src/components/sidebar.tsx @@ -6,7 +6,7 @@ import { auth } from '@/utils/auth'; import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { cn } from '@trycompai/ui/cn'; -import { db, type Organization, Role } from '@db'; +import { db, type Organization, Role } from '@db/server'; import { cookies, headers } from 'next/headers'; import { MainMenu } from './main-menu'; import { OrganizationSwitcher } from './organization-switcher'; diff --git a/apps/app/src/hooks/use-vendors.ts b/apps/app/src/hooks/use-vendors.ts index 12f8b10cb0..adaf862fdb 100644 --- a/apps/app/src/hooks/use-vendors.ts +++ b/apps/app/src/hooks/use-vendors.ts @@ -9,8 +9,8 @@ import type { VendorStatus, Likelihood, Impact, + Prisma, } from '@db'; -import type { JsonValue } from '@prisma/client/runtime/library'; // Default polling interval for real-time updates (5 seconds) const DEFAULT_POLLING_INTERVAL = 5000; @@ -54,7 +54,7 @@ export interface VendorsResponse { */ export interface VendorResponse extends Vendor { // GlobalVendors risk assessment data merged by API - riskAssessmentData?: JsonValue | null; + riskAssessmentData?: Prisma.JsonValue | null; riskAssessmentVersion?: string | null; riskAssessmentUpdatedAt?: string | null; } diff --git a/apps/app/src/lib/api-key.ts b/apps/app/src/lib/api-key.ts index 39db905518..1532fb2da3 100644 --- a/apps/app/src/lib/api-key.ts +++ b/apps/app/src/lib/api-key.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { createHash, randomBytes } from 'node:crypto'; diff --git a/apps/app/src/lib/compliance.ts b/apps/app/src/lib/compliance.ts index c56c1aad8c..c047b0882b 100644 --- a/apps/app/src/lib/compliance.ts +++ b/apps/app/src/lib/compliance.ts @@ -1,7 +1,7 @@ import 'server-only'; import { BUILT_IN_ROLE_OBLIGATIONS } from '@trycompai/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { type UserPermissions, canAccessApp, diff --git a/apps/app/src/lib/currentOrganization.ts b/apps/app/src/lib/currentOrganization.ts index 5e2c909d41..e0bddea6cc 100644 --- a/apps/app/src/lib/currentOrganization.ts +++ b/apps/app/src/lib/currentOrganization.ts @@ -2,7 +2,7 @@ import { auth } from '@/utils/auth'; import type { Organization } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; export async function getCurrentOrganization({ diff --git a/apps/app/src/lib/db/employee.ts b/apps/app/src/lib/db/employee.ts index 88e2558da9..a548ac2897 100644 --- a/apps/app/src/lib/db/employee.ts +++ b/apps/app/src/lib/db/employee.ts @@ -1,6 +1,6 @@ import { env } from '@/env.mjs'; import { trainingVideos } from '@/lib/data/training-videos'; -import { db, type Departments, type Member, type Role } from '@db'; +import { db, type Departments, type Member, type Role } from '@db/server'; import { revalidatePath } from 'next/cache'; if (!env.NEXT_PUBLIC_PORTAL_URL) { diff --git a/apps/app/src/lib/org-chart.ts b/apps/app/src/lib/org-chart.ts index b73d809a33..508bbd85ee 100644 --- a/apps/app/src/lib/org-chart.ts +++ b/apps/app/src/lib/org-chart.ts @@ -1,4 +1,4 @@ -import { db, Prisma } from '@db'; +import { db, Prisma } from '@db/server'; /** * Removes a member's node (and all connected edges) from the org chart. diff --git a/apps/app/src/lib/permissions.server.ts b/apps/app/src/lib/permissions.server.ts index bac2ce1d9f..10beb4c323 100644 --- a/apps/app/src/lib/permissions.server.ts +++ b/apps/app/src/lib/permissions.server.ts @@ -1,7 +1,7 @@ import 'server-only'; import { auth } from '@/utils/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { diff --git a/apps/app/src/lib/vector/sync/sync-manual-answer.ts b/apps/app/src/lib/vector/sync/sync-manual-answer.ts index fd4cebf918..09fcae8b92 100644 --- a/apps/app/src/lib/vector/sync/sync-manual-answer.ts +++ b/apps/app/src/lib/vector/sync/sync-manual-answer.ts @@ -2,7 +2,7 @@ import 'server-only'; import { upsertEmbedding } from '../core/upsert-embedding'; import { vectorIndex } from '../core/client'; -import { db } from '@db'; +import { db } from '@db/server'; import { logger } from '@/utils/logger'; /** diff --git a/apps/app/src/lib/vector/sync/sync-organization.ts b/apps/app/src/lib/vector/sync/sync-organization.ts index 9a32e9d586..5978cc5c29 100644 --- a/apps/app/src/lib/vector/sync/sync-organization.ts +++ b/apps/app/src/lib/vector/sync/sync-organization.ts @@ -1,7 +1,7 @@ import 'server-only'; import { logger } from '@/utils/logger'; -import { db } from '@db'; +import { db } from '@db/server'; import { vectorIndex } from '../core/client'; import { findAllOrganizationEmbeddings, diff --git a/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts b/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts index d35fdc3655..eff697bdc9 100644 --- a/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts +++ b/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts @@ -1,6 +1,6 @@ import { getOrganizationContext } from '@/trigger/tasks/onboarding/onboard-organization-helpers'; import { openai } from '@ai-sdk/openai'; -import { db } from '@db'; +import { db } from '@db/server'; import { logger, metadata, schemaTask } from '@trigger.dev/sdk'; import { generateText } from 'ai'; import { z } from 'zod'; diff --git a/apps/app/src/trigger/tasks/device/create-fleet-label-for-all-orgs.ts b/apps/app/src/trigger/tasks/device/create-fleet-label-for-all-orgs.ts index f2e4f71646..8e59bc1eb1 100644 --- a/apps/app/src/trigger/tasks/device/create-fleet-label-for-all-orgs.ts +++ b/apps/app/src/trigger/tasks/device/create-fleet-label-for-all-orgs.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, task } from '@trigger.dev/sdk'; import { createFleetLabelForOrg } from './create-fleet-label-for-org'; diff --git a/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts b/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts index 73548ef279..68e3d3db4b 100644 --- a/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts +++ b/apps/app/src/trigger/tasks/device/create-fleet-label-for-org.ts @@ -1,5 +1,5 @@ import { getFleetInstance } from '@/lib/fleet'; -import { db } from '@db'; +import { db } from '@db/server'; import { logger, queue, task } from '@trigger.dev/sdk'; import { AxiosError } from 'axios'; diff --git a/apps/app/src/trigger/tasks/email/new-policy-email.ts b/apps/app/src/trigger/tasks/email/new-policy-email.ts index d422adcfdd..c50bf4e512 100644 --- a/apps/app/src/trigger/tasks/email/new-policy-email.ts +++ b/apps/app/src/trigger/tasks/email/new-policy-email.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { PolicyNotificationEmail } from '@trycompai/email'; import { isUserUnsubscribed } from '@trycompai/email/lib/check-unsubscribe'; import { logger, queue, task } from '@trigger.dev/sdk'; diff --git a/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts b/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts index 72d54a2f25..ec4dfcc624 100644 --- a/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts +++ b/apps/app/src/trigger/tasks/email/publish-all-policies-email.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { AllPolicyNotificationEmail } from '@trycompai/email'; import { isUserUnsubscribed } from '@trycompai/email/lib/check-unsubscribe'; import { logger, queue, task } from '@trigger.dev/sdk'; diff --git a/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts b/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts index 8e18f5a932..0741cb831f 100644 --- a/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts +++ b/apps/app/src/trigger/tasks/email/weekly-task-digest-email.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, queue, task } from '@trigger.dev/sdk'; import WeeklyTaskDigestEmail from '@trycompai/email/emails/reminders/weekly-task-digest'; import { isUserUnsubscribed } from '@trycompai/email/lib/check-unsubscribe'; diff --git a/apps/app/src/trigger/tasks/integration/integration-results.ts b/apps/app/src/trigger/tasks/integration/integration-results.ts index a5b444e7eb..4a2dd86583 100644 --- a/apps/app/src/trigger/tasks/integration/integration-results.ts +++ b/apps/app/src/trigger/tasks/integration/integration-results.ts @@ -1,6 +1,6 @@ import { decrypt } from '@trycompai/app/src/lib/encryption'; import { type DecryptFunction, getIntegrationHandler } from '@trycompai/integrations'; -import { db } from '@db'; +import { db } from '@db/server'; import { logger, schemaTask } from '@trigger.dev/sdk'; import { z } from 'zod'; diff --git a/apps/app/src/trigger/tasks/integration/integration-schedule.ts b/apps/app/src/trigger/tasks/integration/integration-schedule.ts index a20c7581ba..4eaa7f4b20 100644 --- a/apps/app/src/trigger/tasks/integration/integration-schedule.ts +++ b/apps/app/src/trigger/tasks/integration/integration-schedule.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, schedules } from '@trigger.dev/sdk'; import { sendIntegrationResults } from './integration-results'; diff --git a/apps/app/src/trigger/tasks/integration/run-integration-tests.ts b/apps/app/src/trigger/tasks/integration/run-integration-tests.ts index 103b64cf7a..f4dd050133 100644 --- a/apps/app/src/trigger/tasks/integration/run-integration-tests.ts +++ b/apps/app/src/trigger/tasks/integration/run-integration-tests.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, task } from '@trigger.dev/sdk'; import { sendIntegrationResults } from './integration-results'; diff --git a/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-all-orgs.ts b/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-all-orgs.ts index 791fbc8bdb..3acf655093 100644 --- a/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-all-orgs.ts +++ b/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-all-orgs.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, task } from '@trigger.dev/sdk'; import { backfillExecutiveContextSingleOrg } from './backfill-executive-context-single-org'; diff --git a/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-single-org.ts b/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-single-org.ts index 442a5c6042..0ac018dc48 100644 --- a/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-single-org.ts +++ b/apps/app/src/trigger/tasks/onboarding/backfill-executive-context-single-org.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, queue, schemaTask } from '@trigger.dev/sdk'; import { z } from 'zod'; diff --git a/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-all-orgs.ts b/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-all-orgs.ts index d76a19834d..9c7675529f 100644 --- a/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-all-orgs.ts +++ b/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-all-orgs.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, task } from '@trigger.dev/sdk'; import { backfillTrainingVideosForOrg } from './backfill-training-videos-for-org'; diff --git a/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts b/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts index 2b805517cb..79811d588d 100644 --- a/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts +++ b/apps/app/src/trigger/tasks/onboarding/backfill-training-videos-for-org.ts @@ -1,5 +1,5 @@ import { trainingVideos } from '@/lib/data/training-videos'; -import { db } from '@db'; +import { db } from '@db/server'; import { logger, task } from '@trigger.dev/sdk'; export const backfillTrainingVideosForOrg = task({ diff --git a/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts b/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts index 6d45810b71..b749a5e404 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-full-policies.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, queue, task } from '@trigger.dev/sdk'; import { getOrganizationContext, triggerPolicyUpdates } from './onboard-organization-helpers'; diff --git a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts index fc26e82132..099ef1bfee 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts @@ -1,4 +1,4 @@ -import { RiskStatus, db } from '@db'; +import { RiskStatus, db } from '@db/server'; import { logger, metadata, queue, task } from '@trigger.dev/sdk'; import axios from 'axios'; import { diff --git a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts index f33fd95e9b..8467d72124 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts @@ -1,4 +1,4 @@ -import { VendorStatus, db } from '@db'; +import { VendorStatus, db } from '@db/server'; import { logger, metadata, queue, task } from '@trigger.dev/sdk'; import axios from 'axios'; import { diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index a26f862f22..1b1bebdb52 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -1,7 +1,6 @@ import { openai } from '@ai-sdk/openai'; import { CommentEntityType, - db, Departments, FrameworkEditorFramework, Impact, @@ -13,6 +12,7 @@ import { VendorCategory, VendorStatus, } from '@db'; +import { db } from '@db/server'; import { logger, metadata, tasks } from '@trigger.dev/sdk'; import { generateObject, generateText, jsonSchema } from 'ai'; import axios from 'axios'; diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index 47d696342b..5b17caeb1d 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, metadata, queue, task, tasks } from '@trigger.dev/sdk'; import axios from 'axios'; import { generateAuditorContentTask } from '../auditor/generate-auditor-content'; diff --git a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts index e3641ce69e..31582ab537 100644 --- a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts @@ -1,5 +1,5 @@ import { openai } from '@ai-sdk/openai'; -import { db, FrameworkEditorFramework, FrameworkEditorPolicyTemplate, type Policy } from '@db'; +import { db, FrameworkEditorFramework, FrameworkEditorPolicyTemplate, type Policy } from '@db/server'; import type { JSONContent } from '@tiptap/react'; import { logger } from '@trigger.dev/sdk'; import { generateObject, NoObjectGeneratedError } from 'ai'; diff --git a/apps/app/src/trigger/tasks/scrape/research.ts b/apps/app/src/trigger/tasks/scrape/research.ts index c6722bf5ad..a1c1e75cac 100644 --- a/apps/app/src/trigger/tasks/scrape/research.ts +++ b/apps/app/src/trigger/tasks/scrape/research.ts @@ -1,5 +1,5 @@ import { researchJobCore } from '@/trigger/lib/research'; -import { db } from '@db'; +import { db } from '@db/server'; import { schemaTask } from '@trigger.dev/sdk'; import { z } from 'zod'; diff --git a/apps/app/src/trigger/tasks/task/policy-schedule.ts b/apps/app/src/trigger/tasks/task/policy-schedule.ts index be7de7dc4d..444df004e8 100644 --- a/apps/app/src/trigger/tasks/task/policy-schedule.ts +++ b/apps/app/src/trigger/tasks/task/policy-schedule.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { Novu } from '@novu/api'; import { logger, schedules } from '@trigger.dev/sdk'; diff --git a/apps/app/src/trigger/tasks/task/task-schedule-helpers.ts b/apps/app/src/trigger/tasks/task/task-schedule-helpers.ts index 518a8eb8ea..ba0b6d355f 100644 --- a/apps/app/src/trigger/tasks/task/task-schedule-helpers.ts +++ b/apps/app/src/trigger/tasks/task/task-schedule-helpers.ts @@ -7,7 +7,7 @@ import type { EvidenceAutomationEvaluationStatus, IntegrationRunStatus, TaskStatus, -} from '@trycompai/db'; +} from '@db'; export type TargetStatus = Extract; diff --git a/apps/app/src/trigger/tasks/task/task-schedule.ts b/apps/app/src/trigger/tasks/task/task-schedule.ts index 033a55a470..a112c59aba 100644 --- a/apps/app/src/trigger/tasks/task/task-schedule.ts +++ b/apps/app/src/trigger/tasks/task/task-schedule.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { Novu } from '@novu/api'; import { logger, schedules } from '@trigger.dev/sdk'; import { isUserUnsubscribed, TaskStatusNotificationEmail } from '@trycompai/email'; diff --git a/apps/app/src/trigger/tasks/task/weekly-task-reminder.ts b/apps/app/src/trigger/tasks/task/weekly-task-reminder.ts index de3418bea9..3e35fdc5bb 100644 --- a/apps/app/src/trigger/tasks/task/weekly-task-reminder.ts +++ b/apps/app/src/trigger/tasks/task/weekly-task-reminder.ts @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; import { logger, schedules } from '@trigger.dev/sdk'; import { sendWeeklyTaskDigestEmailTask } from '../email/weekly-task-digest-email'; diff --git a/apps/app/trigger.config.ts b/apps/app/trigger.config.ts index 8e30ff14de..439e6eeaa6 100644 --- a/apps/app/trigger.config.ts +++ b/apps/app/trigger.config.ts @@ -12,8 +12,8 @@ export default defineConfig({ build: { extensions: [ prismaExtension({ - version: '6.18.0', - dbPackageVersion: '^1.3.21', // Version of @trycompai/db package with compiled JS + version: '7.6.0', + dbPackageVersion: '^2.0.0', }), puppeteer(), syncVercelEnvVars(), diff --git a/apps/app/tsconfig.json b/apps/app/tsconfig.json index d1e8eeec0a..f871e9cbe0 100644 --- a/apps/app/tsconfig.json +++ b/apps/app/tsconfig.json @@ -30,6 +30,9 @@ "@db": [ "./prisma" ], + "@db/server": [ + "./prisma/server" + ], "@/trigger": [ "./src/trigger" ], diff --git a/apps/framework-editor/.gitignore b/apps/framework-editor/.gitignore new file mode 100644 index 0000000000..d3db8f9b18 --- /dev/null +++ b/apps/framework-editor/.gitignore @@ -0,0 +1 @@ +src/generated/ diff --git a/apps/framework-editor/package.json b/apps/framework-editor/package.json index 5b315c48b0..90e5f35b8b 100644 --- a/apps/framework-editor/package.json +++ b/apps/framework-editor/package.json @@ -4,7 +4,8 @@ "@hookform/resolvers": "^5.2.2", "@noble/ciphers": "^1.1.0", "@noble/hashes": "^1.6.1", - "@prisma/client": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", "@tailwindcss/postcss": "^4.1.17", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.8", @@ -34,7 +35,7 @@ "@types/react-dom": "^19.1.1", "autoprefixer": "^10.4.21", "postcss": "^8.5.4", - "prisma": "6.18.0", + "prisma": "7.6.0", "tailwindcss": "^4.1.8", "typescript": "^5.8.3" }, diff --git a/apps/framework-editor/prisma.config.ts b/apps/framework-editor/prisma.config.ts new file mode 100644 index 0000000000..f0e7629866 --- /dev/null +++ b/apps/framework-editor/prisma.config.ts @@ -0,0 +1,9 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index 2381727591..7a53dd537d 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -1,3 +1,13 @@ -import { db } from '@trycompai/db'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; -export { db }; +const globalForPrisma = global as unknown as { prisma: PrismaClient }; + +function createPrismaClient(): PrismaClient { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const db = globalForPrisma.prisma || createPrismaClient(); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/apps/framework-editor/prisma/index.ts b/apps/framework-editor/prisma/index.ts index d9b899151d..b329db54e3 100644 --- a/apps/framework-editor/prisma/index.ts +++ b/apps/framework-editor/prisma/index.ts @@ -1,2 +1 @@ -export * from '@trycompai/db'; -export { db } from './client'; +export * from '@prisma/client'; diff --git a/apps/api/prisma/index.d.ts b/apps/framework-editor/prisma/server.ts similarity index 100% rename from apps/api/prisma/index.d.ts rename to apps/framework-editor/prisma/server.ts diff --git a/apps/framework-editor/tsconfig.json b/apps/framework-editor/tsconfig.json index 789231a739..45070f80a4 100644 --- a/apps/framework-editor/tsconfig.json +++ b/apps/framework-editor/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "jsxImportSource": "react", "incremental": true, "plugins": [ @@ -29,6 +29,9 @@ ], "@/db": [ "./prisma" + ], + "@/db/server": [ + "./prisma/server" ] } }, diff --git a/apps/portal/.gitignore b/apps/portal/.gitignore index 16f0376cb8..f2717bc8f5 100644 --- a/apps/portal/.gitignore +++ b/apps/portal/.gitignore @@ -42,5 +42,6 @@ next-env.d.ts # Generated Prisma Client prisma/generated +src/generated/ # Copied schema from @trycompai/db package - always generate fresh prisma/schema.prisma diff --git a/apps/portal/package.json b/apps/portal/package.json index e31d86bced..716a2e722b 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -5,14 +5,15 @@ "@aws-sdk/client-s3": "^3.859.0", "@aws-sdk/s3-request-presigner": "^3.859.0", "@hookform/resolvers": "^5.2.2", - "@prisma/client": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", "@react-email/components": "^0.0.41", "@react-email/render": "^1.1.2", "@t3-oss/env-nextjs": "^0.13.8", "@trycompai/auth": "workspace:*", "@trycompai/company": "workspace:*", "@trycompai/analytics": "workspace:*", - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "@trycompai/design-system": "^1.0.43", "@trycompai/email": "workspace:*", "@trycompai/kv": "workspace:*", @@ -28,7 +29,7 @@ "next": "^16.0.10", "next-safe-action": "^8.0.3", "next-themes": "^0.4.4", - "prisma": "6.18.0", + "prisma": "7.6.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-email": "^4.0.15", @@ -54,7 +55,7 @@ "scripts": { "build": "next build", "build:docker": "prisma generate && next build", - "db:generate": "bun run db:getschema && prisma generate", + "db:generate": "bun run db:getschema && prisma generate && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/portal", "dev": "next dev --turbopack -p 3002", diff --git a/apps/portal/prisma.config.ts b/apps/portal/prisma.config.ts new file mode 100644 index 0000000000..f0e7629866 --- /dev/null +++ b/apps/portal/prisma.config.ts @@ -0,0 +1,9 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index a696328bef..9abf612b4f 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -1,7 +1,13 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient } from '../src/generated/prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -export const db = globalForPrisma.prisma || new PrismaClient(); +function createPrismaClient(): PrismaClient { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const db = globalForPrisma.prisma || createPrismaClient(); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/apps/portal/prisma/index.ts b/apps/portal/prisma/index.ts index 54d1c4b9c9..ccdcea1dfa 100644 --- a/apps/portal/prisma/index.ts +++ b/apps/portal/prisma/index.ts @@ -1,2 +1 @@ -export * from '@prisma/client'; -export { db } from './client'; +export * from '../src/generated/prisma/browser'; diff --git a/apps/portal/prisma/server.ts b/apps/portal/prisma/server.ts new file mode 100644 index 0000000000..7969070f3c --- /dev/null +++ b/apps/portal/prisma/server.ts @@ -0,0 +1,2 @@ +export * from '../src/generated/prisma/client'; +export { db } from './client'; diff --git a/apps/portal/src/actions/accept-policies.ts b/apps/portal/src/actions/accept-policies.ts index 8c415188ea..aa19d11cbd 100644 --- a/apps/portal/src/actions/accept-policies.ts +++ b/apps/portal/src/actions/accept-policies.ts @@ -1,6 +1,6 @@ 'use server'; -import { db } from '@db'; +import { db } from '@db/server'; export async function acceptPolicy(policyId: string, memberId: string) { try { diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx index 3f54751ebe..b012692d86 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx @@ -1,5 +1,5 @@ import type { Device, Member, Organization, User } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import { NoAccessMessage } from '../../components/NoAccessMessage'; import type { FleetPolicy, Host } from '../types'; import { EmployeeTasksList } from './EmployeeTasksList'; diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx index a9599c5d34..6e5a3518cc 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/page.tsx @@ -1,6 +1,6 @@ import { auth } from '@/app/lib/auth'; import { env } from '@/env.mjs'; -import { db } from '@db'; +import { db } from '@db/server'; import { Breadcrumb, PageLayout } from '@trycompai/design-system'; import { isRedirectError } from 'next/dist/client/components/redirect-error'; import { headers as getHeaders } from 'next/headers'; @@ -68,7 +68,6 @@ export default async function PortalCompanyFormPage({ const apiHeaders = { 'Content-Type': 'application/json', - 'X-Organization-Id': orgId, Cookie: cookie, }; diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx index 19887d719f..1ddc7e59fb 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/documents/[formType]/submissions/page.tsx @@ -1,6 +1,6 @@ import { auth } from '@/app/lib/auth'; import { env } from '@/env.mjs'; -import { db } from '@db'; +import { db } from '@db/server'; import { Breadcrumb, PageLayout } from '@trycompai/design-system'; import { headers as getHeaders } from 'next/headers'; import Link from 'next/link'; @@ -69,6 +69,11 @@ export default async function PortalSubmissionsPage({ const apiUrl = env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'; const cookie = reqHeaders.get('cookie') ?? ''; + const apiHeaders = { + 'Content-Type': 'application/json', + Cookie: cookie, + }; + let submissions: SubmissionRow[] = []; if (cookie) { @@ -77,11 +82,7 @@ export default async function PortalSubmissionsPage({ `${apiUrl}/v1/evidence-forms/my-submissions?formType=${formTypeValue}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Organization-Id': orgId, - Cookie: cookie, - }, + headers: apiHeaders, cache: 'no-store', }, ); diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx index a0d45853bc..4e71a2eb6e 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx @@ -3,7 +3,7 @@ import { auth } from '@/app/lib/auth'; import { getFleetInstance } from '@/utils/fleet'; import type { FleetPolicyResult, Member } from '@db'; -import { db } from '@db'; +import { db } from '@db/server'; import { PageHeader, PageLayout } from '@trycompai/design-system'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx index d817a5c2c0..d743ad5588 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx @@ -1,5 +1,5 @@ import { auth } from '@/app/lib/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { Badge, Breadcrumb, diff --git a/apps/portal/src/app/(app)/(home)/components/Overview.tsx b/apps/portal/src/app/(app)/(home)/components/Overview.tsx index fbc0d18db6..7b24c91f70 100644 --- a/apps/portal/src/app/(app)/(home)/components/Overview.tsx +++ b/apps/portal/src/app/(app)/(home)/components/Overview.tsx @@ -1,4 +1,4 @@ -import { db } from '@db'; +import { db } from '@db/server'; // Import types directly from @prisma/client import type { Member, Organization, User } from '@db'; import { headers } from 'next/headers'; diff --git a/apps/portal/src/app/api/confirm-fleet-policy/route.ts b/apps/portal/src/app/api/confirm-fleet-policy/route.ts index fe2510ce76..114234aac0 100644 --- a/apps/portal/src/app/api/confirm-fleet-policy/route.ts +++ b/apps/portal/src/app/api/confirm-fleet-policy/route.ts @@ -2,7 +2,7 @@ import { auth } from '@/app/lib/auth'; import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/utils/s3'; import { validateMemberAndOrg } from '@/app/api/download-agent/utils'; import { DeleteObjectsCommand, PutObjectCommand } from '@aws-sdk/client-s3'; -import { db } from '@db'; +import { db } from '@db/server'; import { Buffer } from 'node:buffer'; import { type NextRequest, NextResponse } from 'next/server'; diff --git a/apps/portal/src/app/api/download-agent/utils.ts b/apps/portal/src/app/api/download-agent/utils.ts index 5fe2018062..6009b672be 100644 --- a/apps/portal/src/app/api/download-agent/utils.ts +++ b/apps/portal/src/app/api/download-agent/utils.ts @@ -1,5 +1,5 @@ import { logger } from '@/utils/logger'; -import { db } from '@db'; +import { db } from '@db/server'; import type { SupportedOS } from './types'; /** diff --git a/apps/portal/src/app/api/fleet-policies/route.ts b/apps/portal/src/app/api/fleet-policies/route.ts index f8a26349b9..b9b25fc7fc 100644 --- a/apps/portal/src/app/api/fleet-policies/route.ts +++ b/apps/portal/src/app/api/fleet-policies/route.ts @@ -1,6 +1,6 @@ import { auth } from "@/app/lib/auth"; import { NextRequest, NextResponse } from "next/server"; -import { db } from "@db"; +import { db } from "@db/server"; import { validateMemberAndOrg } from "../download-agent/utils"; import { getFleetInstance } from "@/utils/fleet"; import { FleetPolicy, Host } from "@/app/(app)/(home)/[orgId]/types"; diff --git a/apps/portal/src/app/api/fleet-policy/route.ts b/apps/portal/src/app/api/fleet-policy/route.ts index 0b94e26920..14a9a9e06b 100644 --- a/apps/portal/src/app/api/fleet-policy/route.ts +++ b/apps/portal/src/app/api/fleet-policy/route.ts @@ -3,7 +3,7 @@ import { validateMemberAndOrg } from '@/app/api/download-agent/utils'; import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/utils/s3'; import { DeleteObjectsCommand, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db } from '@db'; +import { db } from '@db/server'; import { NextRequest, NextResponse } from 'next/server'; export const runtime = 'nodejs'; diff --git a/apps/portal/src/app/api/portal/accept-policies/route.ts b/apps/portal/src/app/api/portal/accept-policies/route.ts index d450688984..6fb0e43d64 100644 --- a/apps/portal/src/app/api/portal/accept-policies/route.ts +++ b/apps/portal/src/app/api/portal/accept-policies/route.ts @@ -1,5 +1,5 @@ import { auth } from '@/app/lib/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { type NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; diff --git a/apps/portal/src/app/api/portal/mark-policy-completed/route.ts b/apps/portal/src/app/api/portal/mark-policy-completed/route.ts index 8dc5c3aec8..f360a25a3e 100644 --- a/apps/portal/src/app/api/portal/mark-policy-completed/route.ts +++ b/apps/portal/src/app/api/portal/mark-policy-completed/route.ts @@ -1,5 +1,5 @@ import { auth } from '@/app/lib/auth'; -import { db } from '@db'; +import { db } from '@db/server'; import { type NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; diff --git a/apps/portal/src/app/api/portal/policy-pdf-url/route.ts b/apps/portal/src/app/api/portal/policy-pdf-url/route.ts index 97dcf715b2..2423e7aeb0 100644 --- a/apps/portal/src/app/api/portal/policy-pdf-url/route.ts +++ b/apps/portal/src/app/api/portal/policy-pdf-url/route.ts @@ -2,7 +2,7 @@ import { auth } from '@/app/lib/auth'; import { BUCKET_NAME, s3Client } from '@/utils/s3'; import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { db } from '@db'; +import { db } from '@db/server'; import { type NextRequest, NextResponse } from 'next/server'; export const runtime = 'nodejs'; diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json index a8618e3d74..4b9995094d 100644 --- a/apps/portal/tsconfig.json +++ b/apps/portal/tsconfig.json @@ -22,6 +22,7 @@ "paths": { "@/*": ["./src/*"], "@db": ["./prisma"], + "@db/server": ["./prisma/server"], "@trycompai/email": ["../../packages/email/index.ts"], "@trycompai/email/*": ["../../packages/email/*"], "@trycompai/kv": ["../../packages/kv/src/index.ts"], diff --git a/bun.lock b/bun.lock index 3409c411cb..f409289013 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,7 @@ "@commitlint/config-conventional": "^19.8.1", "@hookform/resolvers": "^5.2.2", "@number-flow/react": "^0.5.14", - "@prisma/adapter-pg": "6.10.1", + "@prisma/adapter-pg": "7.6.0", "@react-email/components": "^0.0.41", "@react-email/render": "^1.4.0", "@semantic-release/changelog": "^6.0.3", @@ -86,8 +86,9 @@ "@nestjs/platform-express": "^11.1.5", "@nestjs/swagger": "^11.2.0", "@nestjs/throttler": "^6.5.0", - "@prisma/client": "6.18.0", - "@prisma/instrumentation": "^6.13.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", + "@prisma/instrumentation": "7.6.0", "@react-email/components": "^0.0.41", "@react-email/render": "^2.0.4", "@thallesp/nestjs-better-auth": "^2.4.0", @@ -95,7 +96,7 @@ "@trigger.dev/sdk": "4.4.3", "@trycompai/auth": "workspace:*", "@trycompai/company": "workspace:*", - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "@trycompai/email": "workspace:*", "@trycompai/integration-platform": "workspace:*", "@upstash/ratelimit": "^2.0.8", @@ -119,7 +120,7 @@ "nanoid": "^5.1.6", "pdf-lib": "^1.17.1", "playwright-core": "^1.57.0", - "prisma": "6.18.0", + "prisma": "7.6.0", "react": "^19.1.1", "react-dom": "^19.1.0", "reflect-metadata": "^0.2.2", @@ -197,9 +198,9 @@ "@novu/api": "^1.6.0", "@novu/nextjs": "^3.10.1", "@number-flow/react": "^0.5.9", - "@prisma/client": "6.18.0", - "@prisma/instrumentation": "6.18.0", - "@prisma/nextjs-monorepo-workaround-plugin": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", + "@prisma/instrumentation": "7.6.0", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -229,7 +230,7 @@ "@trigger.dev/sdk": "4.4.3", "@trycompai/auth": "workspace:*", "@trycompai/company": "workspace:*", - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "@trycompai/design-system": "^1.0.32", "@trycompai/email": "workspace:*", "@trycompai/integration-platform": "workspace:*", @@ -270,7 +271,7 @@ "playwright-core": "^1.52.0", "posthog-js": "^1.236.6", "posthog-node": "^5.8.2", - "prisma": "6.18.0", + "prisma": "7.6.0", "puppeteer-core": "^24.7.2", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -343,7 +344,8 @@ "@hookform/resolvers": "^5.2.2", "@noble/ciphers": "^1.1.0", "@noble/hashes": "^1.6.1", - "@prisma/client": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", "@tailwindcss/postcss": "^4.1.17", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.8", @@ -373,7 +375,7 @@ "@types/react-dom": "^19.1.1", "autoprefixer": "^10.4.21", "postcss": "^8.5.4", - "prisma": "6.18.0", + "prisma": "7.6.0", "tailwindcss": "^4.1.8", "typescript": "^5.8.3", }, @@ -385,14 +387,15 @@ "@aws-sdk/client-s3": "^3.859.0", "@aws-sdk/s3-request-presigner": "^3.859.0", "@hookform/resolvers": "^5.2.2", - "@prisma/client": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", "@react-email/components": "^0.0.41", "@react-email/render": "^1.1.2", "@t3-oss/env-nextjs": "^0.13.8", "@trycompai/analytics": "workspace:*", "@trycompai/auth": "workspace:*", "@trycompai/company": "workspace:*", - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "@trycompai/design-system": "^1.0.43", "@trycompai/email": "workspace:*", "@trycompai/kv": "workspace:*", @@ -408,7 +411,7 @@ "next": "^16.0.10", "next-safe-action": "^8.0.3", "next-themes": "^0.4.4", - "prisma": "6.18.0", + "prisma": "7.6.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-email": "^4.0.15", @@ -450,19 +453,15 @@ "better-auth": "^1.4.22", }, "devDependencies": { - "@prisma/client": "6.18.0", "@trycompai/tsconfig": "workspace:*", "typescript": "^5.7.3", }, - "peerDependencies": { - "@prisma/client": ">=5.0.0", - }, }, "packages/company": { "name": "@trycompai/company", "version": "1.0.0", "dependencies": { - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "zod": "^4.3.6", }, "devDependencies": { @@ -472,18 +471,19 @@ }, "packages/db": { "name": "@trycompai/db", - "version": "1.3.22", + "version": "2.0.0", "bin": { "comp-prisma-postinstall": "./dist/postinstall.js", }, "dependencies": { - "@prisma/client": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", "dotenv": "^16.4.5", - "zod": "^4.1.12", + "zod": "^4.3.6", }, "devDependencies": { "@types/node": "^24.2.0", - "prisma": "6.18.0", + "prisma": "7.6.0", "ts-node": "^10.9.2", "typescript": "^5.9.2", }, @@ -1191,6 +1191,12 @@ "@electric-sql/client": ["@electric-sql/client@1.0.14", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" } }, "sha512-LtPAfeMxXRiYS0hyDQ5hue2PjljUiK9stvzsVyVb4nwxWQxfOWTSF42bHTs/o5i3x1T4kAQ7mwHpxa4A+f8X7Q=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.4.1", "", {}, "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q=="], + + "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.1.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw=="], + + "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" } }, "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], @@ -1493,6 +1499,8 @@ "@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@langchain/core": ["@langchain/core@0.3.79", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-ZLAs5YMM5N2UXN3kExMglltJrKKoW7hs3KMZFlXUnD7a5DFKBYxPFMeXA4rT+uvTxuJRZPCYX0JKI5BhyAWx4A=="], "@langchain/openai": ["@langchain/openai@0.4.9", "", { "dependencies": { "js-tiktoken": "^1.0.12", "openai": "^4.87.3", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, "peerDependencies": { "@langchain/core": ">=0.3.39 <0.4.0" } }, "sha512-NAsaionRHNdqaMjVLPkFCyjUDze+OqRHghA1Cn4fPoAafz+FXcl9c7LlEl9Xo0FH6/8yiCl7Rw2t780C/SBVxQ=="], @@ -1773,27 +1781,35 @@ "@posthog/core": ["@posthog/core@1.9.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-j7KSWxJTUtNyKynLt/p0hfip/3I46dWU2dk+pt7dKRoz2l5CYueHuHK4EO7Wlgno5yo1HO4sc4s30MXMTICHJw=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@6.10.1", "", { "dependencies": { "@prisma/driver-adapter-utils": "6.10.1", "postgres-array": "3.0.4" }, "peerDependencies": { "pg": "^8.11.3" } }, "sha512-4Kpz5EV1jEOsKDuKYMjfJKMiIIcsuR9Ou1B8zLzehYtB7/oi+1ooDoK1K+T7sMisHkP69aYat5j0dskxvJTgdQ=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="], + + "@prisma/client": ["@prisma/client@7.6.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.6.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-7Pe/1ayh3GgWPEg4mmT4ax77LJ1wC+XlnIFvQ94bLP2DsUnOpnruQQR3Jw7r+Frthk94QqDNxo3FjSg8h9PXeQ=="], - "@prisma/client": ["@prisma/client@6.18.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA=="], + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.6.0", "", {}, "sha512-fD7jlqubsZvVODKvsp9lOpXVecx2aWGxC2l35Ioz2t+teUJ5CfR0SAMsi7UkU1VvaZmmm+DS6BdujF622nY7tQ=="], "@prisma/config": ["@prisma/config@6.18.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ=="], - "@prisma/debug": ["@prisma/debug@6.10.1", "", {}, "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA=="], + "@prisma/debug": ["@prisma/debug@7.6.0", "", {}, "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@6.10.1", "", { "dependencies": { "@prisma/debug": "6.10.1" } }, "sha512-MJ7NiiMA5YQUD1aMHiOcLmRpW0U0NTpygyeuLMxHXnKbcq+HX/cy10qilFMLVzpveuIEHuwxziR67z6i0K1MKA=="], + "@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="], - "@prisma/engines": ["@prisma/engines@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/fetch-engine": "6.18.0", "@prisma/get-platform": "6.18.0" } }, "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA=="], - "@prisma/engines-version": ["@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "", {}, "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ=="], + "@prisma/engines": ["@prisma/engines@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/fetch-engine": "7.6.0", "@prisma/get-platform": "7.6.0" } }, "sha512-Sn5edRzhHqgRV2M+A0eIbY442B4mReWWf3pKs/LKreYgW7oa/up8JtK/s4iv/EQA097cyboZ08mmkpbLp+tZ3w=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/get-platform": "6.18.0" } }, "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A=="], + "@prisma/engines-version": ["@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "", {}, "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw=="], - "@prisma/get-platform": ["@prisma/get-platform@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0" } }, "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/get-platform": "7.6.0" } }, "sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w=="], - "@prisma/instrumentation": ["@prisma/instrumentation@6.19.1", "", { "dependencies": { "@opentelemetry/instrumentation": ">=0.52.0 <1" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-wrfyrZ8oZuJy/RjGyEsyFCPvZQ9NrvWX5MTi0ITFjmj6nS00NaMcwenbcNwb5S5NjFIzHtUNJsYhr3JLb8lN7g=="], + "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], - "@prisma/nextjs-monorepo-workaround-plugin": ["@prisma/nextjs-monorepo-workaround-plugin@6.18.0", "", {}, "sha512-LyKtuMLJ4CtBbt3Kn7LFk/cyFyPV6wONrEpC+WrzfuMzptbY5VJr4Di49oCfwHTs5uxtkpPMMVTY19XgTStb8w=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.6.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ=="], + + "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], + + "@prisma/streams-local": ["@prisma/streams-local@0.1.2", "", { "dependencies": { "ajv": "^8.12.0", "better-result": "^2.7.0", "env-paths": "^3.0.0", "proper-lockfile": "^4.1.2" } }, "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg=="], + + "@prisma/studio-core": ["@prisma/studio-core@0.27.3", "", { "dependencies": { "@radix-ui/react-toggle": "1.1.10", "chart.js": "4.5.1" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw=="], "@protobuf-ts/runtime": ["@protobuf-ts/runtime@2.11.1", "", {}, "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ=="], @@ -2667,6 +2683,8 @@ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], @@ -3043,6 +3061,8 @@ "aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], @@ -3099,6 +3119,8 @@ "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], + "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], @@ -3227,6 +3249,8 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -3565,6 +3589,8 @@ "delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -3999,6 +4025,8 @@ "geist": ["geist@1.5.1", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -4015,6 +4043,8 @@ "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stream": ["get-stream@7.0.1", "", {}, "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ=="], @@ -4071,6 +4101,10 @@ "gradient-string": ["gradient-string@2.0.2", "", { "dependencies": { "chalk": "^4.1.2", "tinygradient": "^1.1.5" } }, "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw=="], + "grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], + + "graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], + "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], @@ -4175,6 +4209,8 @@ "http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="], + "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -4321,6 +4357,8 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], @@ -4671,6 +4709,8 @@ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -4913,10 +4953,14 @@ "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "n8ao": ["n8ao@1.10.1", "", { "peerDependencies": { "postprocessing": ">=6.30.0", "three": ">=0.137" } }, "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w=="], + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + "nano-css": ["nano-css@5.6.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw=="], "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], @@ -5225,6 +5269,8 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -5259,7 +5305,7 @@ "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - "prisma": ["prisma@6.18.0", "", { "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g=="], + "prisma": ["prisma@7.6.0", "", { "dependencies": { "@prisma/config": "7.6.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.6.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-OKJIPT81K3+F+AayIkY/Y3mkF2NWoFh7lZApaaqPYy7EHILKdO0VsmGkP+hDKYTySHsFSyLWXm/JgcR1B8fY1Q=="], "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], @@ -5287,6 +5333,8 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "prosemirror-changeset": ["prosemirror-changeset@2.3.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ=="], @@ -5503,6 +5551,8 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "remend": ["remend@1.2.1", "", {}, "sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w=="], "request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="], @@ -5623,6 +5673,8 @@ "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], @@ -5747,6 +5799,8 @@ "sqids": ["sqids@0.3.0", "", {}, "sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw=="], + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], @@ -6209,6 +6263,8 @@ "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], "validate-npm-package-name": ["validate-npm-package-name@6.0.2", "", {}, "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ=="], @@ -6359,6 +6415,8 @@ "zaraz-ts": ["zaraz-ts@1.2.0", "", {}, "sha512-gdVnNIADRIWpdKZbzaDSo6FIxLiGudhOu9j4gl4TQF9FMGAOBMpfIq7NtSZ8j5wXB8C3Sn3hRY4NNylAGYM3nw=="], + "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], + "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -6711,11 +6769,17 @@ "@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], - "@prisma/engines/@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], + + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], + + "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "@prisma/fetch-engine/@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="], + "@prisma/streams-local/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@prisma/get-platform/@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="], + "@prisma/streams-local/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -6867,8 +6931,6 @@ "@trycompai/app/@mendable/firecrawl-js": ["@mendable/firecrawl-js@1.29.3", "", { "dependencies": { "axios": "^1.11.0", "typescript-event-target": "^1.1.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.0" } }, "sha512-+uvDktesJmVtiwxMtimq+3f5bKlsan4T7TokxOI7DbxFkApwrRNss5GYEXbInveMTz8LpGth/9Ch5BTwCqrpfA=="], - "@trycompai/app/@prisma/instrumentation": ["@prisma/instrumentation@6.18.0", "", { "dependencies": { "@opentelemetry/instrumentation": ">=0.52.0 <1" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-WxyTUjlKKmxV49+UzROi5NR6ZW8zLNyOYaYRL6ACy93wyGyjIjS+ot9TLOssCQYVziU9/IAyjw8QBs4M9Mz/yA=="], - "@trycompai/app/ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], "@trycompai/app/better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], @@ -7367,6 +7429,8 @@ "msw/type-fest": ["type-fest@5.3.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg=="], + "mysql2/iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -7797,12 +7861,18 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "prisma/@prisma/config": ["@prisma/config@7.6.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-MuAz1MK4PeG5/03YzfzX3CnFVHQ6qePGwUpQRzPzX5tT0ffJ3Tzi9zJZbBc+VzEGFCM8ghW/gTVDR85Syjt+Yw=="], + "promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "promise-worker-transferable/is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1534754", "", {}, "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ=="], @@ -8331,6 +8401,14 @@ "@prisma/config/c12/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + + "@prisma/streams-local/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@radix-ui/react-context-menu/@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="], "@radix-ui/react-context-menu/@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], @@ -8467,8 +8545,6 @@ "@trycompai/app/@mendable/firecrawl-js/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@trycompai/app/@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "@trycompai/app/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], "@trycompai/app/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], @@ -8931,6 +9007,10 @@ "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "prisma/@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + + "prisma/@prisma/config/effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="], + "react-email/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "react-email/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -9385,6 +9465,8 @@ "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "@semantic-release/github/aggregate-error/clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "@semantic-release/npm/aggregate-error/clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -9421,12 +9503,6 @@ "@trycompai/app/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@trycompai/app/@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], - - "@trycompai/app/@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], - - "@trycompai/app/@prisma/instrumentation/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], - "@trycompai/app/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@trycompai/app/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -9557,6 +9633,18 @@ "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "prisma/@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "prisma/@prisma/config/c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + + "prisma/@prisma/config/c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "prisma/@prisma/config/c12/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "prisma/@prisma/config/c12/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "prisma/@prisma/config/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "react-syntax-highlighter/refractor/hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], "react-syntax-highlighter/refractor/hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="], @@ -9689,8 +9777,6 @@ "@slack/socket-mode/@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@trycompai/app/@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - "@vercel/sdk/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "@vercel/sdk/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], diff --git a/package.json b/package.json index 1590749400..3f3d3af7b2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@commitlint/config-conventional": "^19.8.1", "@hookform/resolvers": "^5.2.2", "@number-flow/react": "^0.5.14", - "@prisma/adapter-pg": "6.10.1", + "@prisma/adapter-pg": "7.6.0", "@react-email/components": "^0.0.41", "@react-email/render": "^1.4.0", "@semantic-release/changelog": "^6.0.3", @@ -71,7 +71,7 @@ "deps:update": "syncpack update", "deps:upgrade": "syncpack update && bun install", "dev": "turbo dev --parallel", - "db:generate": "turbo run db:generate --filter=@trycompai/app --filter=@trycompai/portal --filter=@trycompai/api", + "db:generate": "turbo run db:generate --filter=@trycompai/db --filter=@trycompai/app --filter=@trycompai/portal --filter=@trycompai/api", "docker:clean": "bun run -F @trycompai/db docker:clean", "docker:down": "bun run -F @trycompai/db docker:down", "docker:up": "bun run -F @trycompai/db docker:up", diff --git a/packages/auth/package.json b/packages/auth/package.json index 8f79d30e5d..f561f7c08b 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -25,12 +25,8 @@ "dependencies": { "better-auth": "^1.4.22" }, - "peerDependencies": { - "@prisma/client": ">=5.0.0" - }, "devDependencies": { "@trycompai/tsconfig": "workspace:*", - "@prisma/client": "6.18.0", "typescript": "^5.7.3" } } diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 4dc55e2cc1..d17cbfc440 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -1,11 +1,16 @@ -import type { PrismaClient } from '@prisma/client'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; import { bearer, emailOTP, jwt, magicLink, multiSession, organization } from 'better-auth/plugins'; import { ac, allRoles } from './permissions'; -export interface CreateAuthServerOptions { - db: PrismaClient; +interface PrismaLike { + $connect(): Promise; + $disconnect(): Promise; + [model: string]: unknown; +} + +export interface CreateAuthServerOptions { + db: TDb; secret: string; baseURL: string; trustedOrigins: string[]; @@ -110,39 +115,45 @@ export function createAuthServer(options: CreateAuthServerOptions) { console.log('[Better Auth] Session creation hook called for user:', session.userId); try { // Find the user's first organization to set as active - const userOrganization = await db.organization.findFirst({ - where: { - members: { - some: { - userId: session.userId, + const dbWithOrganization = db as Record; + const organizationModel = dbWithOrganization.organization as Record; + + if (typeof organizationModel.findFirst === 'function') { + const userOrganization = await organizationModel.findFirst({ + where: { + members: { + some: { + userId: session.userId, + }, }, }, - }, - orderBy: { - createdAt: 'desc', - }, - select: { - id: true, - name: true, - }, - }); - - if (userOrganization) { - console.log( - `[Better Auth] Setting activeOrganizationId to ${userOrganization.id} (${userOrganization.name}) for user ${session.userId}`, - ); - return { - data: { - ...session, - activeOrganizationId: userOrganization.id, + orderBy: { + createdAt: 'desc', + }, + select: { + id: true, + name: true, }, - }; - } else { - console.log(`[Better Auth] No organization found for user ${session.userId}`); - return { - data: session, - }; + }); + + if (userOrganization && typeof userOrganization === 'object' && 'id' in userOrganization && 'name' in userOrganization) { + const typedOrg = userOrganization as { id: string; name: string }; + console.log( + `[Better Auth] Setting activeOrganizationId to ${typedOrg.id} (${typedOrg.name}) for user ${session.userId}`, + ); + return { + data: { + ...session, + activeOrganizationId: typedOrg.id, + }, + }; + } } + + console.log(`[Better Auth] No organization found for user ${session.userId}`); + return { + data: session, + }; } catch (error) { console.error('[Better Auth] Session creation hook error:', error); return { diff --git a/packages/company/package.json b/packages/company/package.json index aba80172da..b117668f03 100644 --- a/packages/company/package.json +++ b/packages/company/package.json @@ -2,7 +2,7 @@ "name": "@trycompai/company", "version": "1.0.0", "dependencies": { - "@trycompai/db": "1.3.22", + "@trycompai/db": "workspace:*", "zod": "^4.3.6" }, "devDependencies": { diff --git a/packages/db/.gitignore b/packages/db/.gitignore index 7b182107cc..3a321afdef 100644 --- a/packages/db/.gitignore +++ b/packages/db/.gitignore @@ -1,5 +1,5 @@ node_modules -generated +src/generated/ # Keep environment variables out of version control .env diff --git a/packages/db/package.json b/packages/db/package.json index 06a6fdda6a..6a3b319e11 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,22 +1,23 @@ { "name": "@trycompai/db", "description": "Database package with Prisma client and schema for Comp AI", - "version": "1.3.22", + "version": "2.0.0", "dependencies": { - "@prisma/client": "6.18.0", + "@prisma/adapter-pg": "7.6.0", + "@prisma/client": "7.6.0", "dotenv": "^16.4.5", - "zod": "^4.1.12" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^24.2.0", - "prisma": "6.18.0", + "prisma": "7.6.0", "ts-node": "^10.9.2", "typescript": "^5.9.2" }, "exports": { ".": { - "import": "./dist/index.js", "types": "./src/index.ts", + "import": "./dist/index.js", "default": "./dist/index.js" }, "./postinstall": { @@ -29,7 +30,9 @@ "comp-prisma-postinstall": "./dist/postinstall.js" }, "files": [ - "dist", "README.md", "INTEGRATION_GUIDE.md" + "dist", + "README.md", + "INTEGRATION_GUIDE.md" ], "publishConfig": { "access": "public" @@ -40,9 +43,9 @@ "directory": "packages/db" }, "scripts": { - "build": "rm -rf dist && rm -rf ./prisma/generated && node scripts/combine-schemas.js && prisma generate --schema=dist/schema.prisma && tsc", + "build": "rm -rf dist && node scripts/combine-schemas.js && node scripts/generate-prisma-client-js.js && tsc", "check-types": "tsc --noEmit", - "db:generate": "node scripts/combine-schemas.js && prisma generate --schema=dist/schema.prisma", + "db:generate": "node scripts/combine-schemas.js && node scripts/generate-prisma-client-js.js", "db:migrate": "prisma migrate dev", "db:migrate:reset": "prisma migrate reset", "db:push": "prisma db push", diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts index 48ff0a0e72..d27b730a7d 100644 --- a/packages/db/prisma.config.ts +++ b/packages/db/prisma.config.ts @@ -1,15 +1,13 @@ -import 'dotenv/config'; -import path from 'node:path'; -import { defineConfig } from 'prisma/config'; +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; export default defineConfig({ - earlyAccess: true, - schema: path.join('prisma'), - migrate: { - url: process.env.DATABASE_URL!, + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), }, migrations: { - path: path.join('prisma', 'migrations'), - seed: 'ts-node prisma/seed/seed.ts', + path: "prisma/migrations", + seed: "bun prisma/seed/seed.ts", }, }); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 0d6f9b70b6..c02b00f006 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -1,12 +1,10 @@ generator client { - provider = "prisma-client-js" - engineType = "binary" + provider = "prisma-client" + output = "../src/generated/prisma" previewFeatures = ["postgresqlExtensions"] - binaryTargets = ["rhel-openssl-3.0.x", "native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"] } datasource db { provider = "postgresql" - url = env("DATABASE_URL") extensions = [pgcrypto] } diff --git a/packages/db/prisma/seed/seed.ts b/packages/db/prisma/seed/seed.ts index 1fe8694952..3d1fc37982 100644 --- a/packages/db/prisma/seed/seed.ts +++ b/packages/db/prisma/seed/seed.ts @@ -1,11 +1,11 @@ import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; import fs from 'node:fs/promises'; import path from 'node:path'; import { frameworkEditorModelSchemas } from './frameworkEditorSchemas'; -const prisma = new PrismaClient({ - datasourceUrl: process.env.DATABASE_URL, -}); +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +const prisma = new PrismaClient({ adapter }); async function seedJsonFiles(subDirectory: string) { const directoryPath = path.join(__dirname, subDirectory); diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 681845ecae..0af706563c 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -49,16 +49,19 @@ if (!fs.existsSync(OUTPUT_DIR)) { fs.writeFileSync(OUTPUT_SCHEMA, combinedSchema); // Copy the client, index, and types files -const clientFileContent = `import { PrismaClient } from '@prisma/client'; +const clientFileContent = `import { PrismaClient } from '../src/generated/prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; + const globalForPrisma = global as unknown as { prisma: PrismaClient }; -export const db = globalForPrisma.prisma || new PrismaClient(); +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +export const db = globalForPrisma.prisma || new PrismaClient({ adapter }); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; `; fs.writeFileSync(path.join(OUTPUT_DIR, 'client.ts'), clientFileContent); -// Create an index file that re-exports the db client -const indexFileContent = `export { db } from './client' -export * from '@prisma/client'; +// Create an index file — browser-safe types only for monorepo consumption. +// The db instance is server-only and must be imported from './client' directly. +const indexFileContent = `export * from '../src/generated/prisma/browser'; `; fs.writeFileSync(path.join(OUTPUT_DIR, 'index.ts'), indexFileContent); diff --git a/packages/db/scripts/fix-generated-extensions.js b/packages/db/scripts/fix-generated-extensions.js new file mode 100644 index 0000000000..8c174a1c1e --- /dev/null +++ b/packages/db/scripts/fix-generated-extensions.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/** + * Prisma's prisma-client generator outputs .ts files with .js extension imports + * (standard ESM convention). Turbopack in Next.js can't resolve .js→.ts cross-package. + * This script rewrites .js imports to .ts in generated files. + * + * Usage: node fix-generated-extensions.js [dir] + * dir: path to generated prisma directory (default: ../src/generated/prisma relative to this script) + */ +const fs = require('fs'); +const path = require('path'); + +const generatedDir = process.argv[2] + ? path.resolve(process.argv[2]) + : path.join(__dirname, '../src/generated/prisma'); + +function fixExtensions(dir) { + if (!fs.existsSync(dir)) return; + + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + fixExtensions(fullPath); + } else if (entry.name.endsWith('.ts')) { + let content = fs.readFileSync(fullPath, 'utf8'); + const updated = content + .replace(/from '(\.[^']*)\.js'/g, "from '$1.ts'") + .replace(/from "(\.[^"]*)\.js"/g, 'from "$1.ts"'); + if (updated !== content) { + fs.writeFileSync(fullPath, updated); + } + } + } +} + +fixExtensions(generatedDir); +console.log(`[fix-extensions] Rewrote .js → .ts imports in ${generatedDir}`); diff --git a/packages/db/scripts/generate-prisma-client-js.js b/packages/db/scripts/generate-prisma-client-js.js new file mode 100644 index 0000000000..28f834df11 --- /dev/null +++ b/packages/db/scripts/generate-prisma-client-js.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Generates a prisma-client-js client to populate @prisma/client at node_modules. + * This is fast (no .ts files to compile) and provides runtime enums + types + * for packages that import from @trycompai/db. + * + * The combined schema uses prisma-client provider. This script patches a temp copy + * to use prisma-client-js (no custom output) and generates to node_modules/@prisma/client. + */ +const { execFileSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const schemaPath = path.join(__dirname, '../dist/schema.prisma'); +const tempSchema = path.join(__dirname, '../dist/.schema-clientjs.prisma'); + +let schema = fs.readFileSync(schemaPath, 'utf8'); + +// Surgically patch: swap provider and remove output line. +// Preserves all other generator settings (previewFeatures, etc.). +schema = schema + .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') + .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); + +fs.writeFileSync(tempSchema, schema); + +try { + execFileSync('bunx', ['prisma', 'generate', `--schema=${tempSchema}`], { stdio: 'inherit' }); +} finally { + fs.unlinkSync(tempSchema); +} diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index d4c299dd26..49adb2f173 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -1,11 +1,10 @@ import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -export const db = - globalForPrisma.prisma || - new PrismaClient({ - datasourceUrl: process.env.DATABASE_URL, - }); +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + +export const db = globalForPrisma.prisma || new PrismaClient({ adapter }); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 54d1c4b9c9..b329db54e3 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,2 +1 @@ export * from '@prisma/client'; -export { db } from './client'; diff --git a/packages/db/src/postinstall.ts b/packages/db/src/postinstall.ts index f5468159a8..4a844eb71d 100644 --- a/packages/db/src/postinstall.ts +++ b/packages/db/src/postinstall.ts @@ -40,7 +40,7 @@ export function generatePrismaClient(options: GenerateOptions = {}): { schema: s copyFileSync(resolution.path, schemaDestination); log(`Copied schema from ${resolution.path} to ${schemaDestination}`); - const clientEntryPoint = resolve(projectRoot, 'node_modules/.prisma/client/default.js'); + const clientEntryPoint = resolve(projectRoot, 'src', 'generated', 'prisma', 'client.ts'); if (!options.force && existsSync(clientEntryPoint)) { log('Prisma client already exists. Skipping generation.'); return { schema: schemaDestination }; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 21e5f9ecf0..041ff12a62 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -19,5 +19,5 @@ "declarationMap": true }, "include": ["src"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/generated"] } diff --git a/turbo.json b/turbo.json index f1a9db6057..971ec513f9 100644 --- a/turbo.json +++ b/turbo.json @@ -67,8 +67,9 @@ "ui": "tui", "tasks": { "db:generate": { + "dependsOn": ["^db:generate"], "cache": false, - "outputs": ["prisma/schema.prisma", "node_modules/.prisma/**"] + "outputs": ["prisma/schema.prisma", "node_modules/.prisma/**", "src/generated/**"] }, "build": { "dependsOn": [ From 3e29382c157dc8e4e66a3443eb9084b2b4facdfb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:28:04 -0400 Subject: [PATCH 14/31] fix(db): use process.env fallback for DATABASE_URL in prisma.config.ts (#2416) Co-authored-by: Mariano Fuentes --- packages/db/prisma.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts index d27b730a7d..ba4d5c00f2 100644 --- a/packages/db/prisma.config.ts +++ b/packages/db/prisma.config.ts @@ -1,10 +1,10 @@ import "dotenv/config"; -import { defineConfig, env } from "prisma/config"; +import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { - url: env("DATABASE_URL"), + url: process.env.DATABASE_URL ?? "", }, migrations: { path: "prisma/migrations", From 977a705a52432b68114d1e082e239e877431c9d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:39:27 -0400 Subject: [PATCH 15/31] fix: use process.env fallback for DATABASE_URL in all prisma.config.ts files (build envs have no DB) (#2417) Co-authored-by: Mariano Fuentes --- apps/api/prisma.config.ts | 4 ++-- apps/app/prisma.config.ts | 4 ++-- apps/framework-editor/prisma.config.ts | 4 ++-- apps/portal/prisma.config.ts | 4 ++-- packages/db/prisma.config.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/api/prisma.config.ts b/apps/api/prisma.config.ts index f0e7629866..a02b89f336 100644 --- a/apps/api/prisma.config.ts +++ b/apps/api/prisma.config.ts @@ -1,9 +1,9 @@ import "dotenv/config"; -import { defineConfig, env } from "prisma/config"; +import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { - url: env("DATABASE_URL"), + url: process.env.DATABASE_URL!, }, }); diff --git a/apps/app/prisma.config.ts b/apps/app/prisma.config.ts index f0e7629866..a02b89f336 100644 --- a/apps/app/prisma.config.ts +++ b/apps/app/prisma.config.ts @@ -1,9 +1,9 @@ import "dotenv/config"; -import { defineConfig, env } from "prisma/config"; +import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { - url: env("DATABASE_URL"), + url: process.env.DATABASE_URL!, }, }); diff --git a/apps/framework-editor/prisma.config.ts b/apps/framework-editor/prisma.config.ts index f0e7629866..a02b89f336 100644 --- a/apps/framework-editor/prisma.config.ts +++ b/apps/framework-editor/prisma.config.ts @@ -1,9 +1,9 @@ import "dotenv/config"; -import { defineConfig, env } from "prisma/config"; +import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { - url: env("DATABASE_URL"), + url: process.env.DATABASE_URL!, }, }); diff --git a/apps/portal/prisma.config.ts b/apps/portal/prisma.config.ts index f0e7629866..a02b89f336 100644 --- a/apps/portal/prisma.config.ts +++ b/apps/portal/prisma.config.ts @@ -1,9 +1,9 @@ import "dotenv/config"; -import { defineConfig, env } from "prisma/config"; +import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { - url: env("DATABASE_URL"), + url: process.env.DATABASE_URL!, }, }); diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts index ba4d5c00f2..00f9b6fcdb 100644 --- a/packages/db/prisma.config.ts +++ b/packages/db/prisma.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { - url: process.env.DATABASE_URL ?? "", + url: process.env.DATABASE_URL!, }, migrations: { path: "prisma/migrations", From 451c6a104c8832de0b55cced9da8f719426b26c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:02:03 -0400 Subject: [PATCH 16/31] fix: add SSL support to PrismaPg adapter for RDS/staging (rejectUnauthorized: false) (#2418) Co-authored-by: Mariano Fuentes --- apps/api/prisma/client.ts | 45 +++++++++++++++++++++++++- apps/app/prisma/client.ts | 20 +++++++++++- apps/framework-editor/prisma/client.ts | 20 +++++++++++- apps/portal/prisma/client.ts | 20 +++++++++++- packages/db/scripts/combine-schemas.js | 22 +++++++++++-- packages/db/src/client.ts | 25 ++++++++++++-- 6 files changed, 144 insertions(+), 8 deletions(-) diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index 7a53dd537d..7200c43c3f 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -1,10 +1,53 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; +import type { PoolConfig } from 'pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +/** + * Derive pg SSL config from the DATABASE_URL sslmode parameter. + * + * pg-connection-string parses sslmode=require into ssl: {} (empty object), + * and pg@8+ treats ssl: {} as { rejectUnauthorized: true }, which rejects + * AWS RDS Proxy's self-signed certificate. This is a known pg@8 breaking + * change (https://node-postgres.com/announcements#ssl-by-default). + * + * Per PostgreSQL spec (https://www.postgresql.org/docs/current/libpq-ssl.html): + * - require: encrypt connection, skip certificate verification + * - verify-ca: encrypt + verify server CA + * - verify-full: encrypt + verify CA + verify hostname + * + * Per Prisma v7 migration docs: "SSL certificate defaults have changed. + * Previously [v6 Rust engine], invalid SSL certificates were ignored." + * (https://www.prisma.io/docs/orm/more/upgrades/to-v7) + * + * Our infra enforces sslmode=require with RDS Proxy (requireTls: true). + * The proxy's certificate is signed by an internal AWS CA not in Node.js's + * root CA store, so rejectUnauthorized: false is required. The connection + * is still TLS-encrypted — only identity verification is skipped. + */ +function getSslConfig(url: string): PoolConfig['ssl'] { + const match = url.match(/sslmode=(\w[\w-]*)/); + if (!match) return undefined; + + const mode = match[1]; + switch (mode) { + case 'disable': + return undefined; + case 'require': + case 'no-verify': + return { rejectUnauthorized: false }; + case 'verify-ca': + case 'verify-full': + return { rejectUnauthorized: true }; + default: + return undefined; + } +} + function createPrismaClient(): PrismaClient { - const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + const url = process.env.DATABASE_URL!; + const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); return new PrismaClient({ adapter }); } diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index 9abf612b4f..59ab0dd8b1 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -1,10 +1,28 @@ import { PrismaClient } from '../src/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; +import type { PoolConfig } from 'pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +/** + * Derive pg SSL config from the DATABASE_URL sslmode parameter. + * See apps/api/prisma/client.ts for detailed documentation. + */ +function getSslConfig(url: string): PoolConfig['ssl'] { + const match = url.match(/sslmode=(\w[\w-]*)/); + if (!match) return undefined; + const mode = match[1]; + switch (mode) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + default: return undefined; + } +} + function createPrismaClient(): PrismaClient { - const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + const url = process.env.DATABASE_URL!; + const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); return new PrismaClient({ adapter }); } diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index 7a53dd537d..f66768121c 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -1,10 +1,28 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; +import type { PoolConfig } from 'pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +/** + * Derive pg SSL config from the DATABASE_URL sslmode parameter. + * See apps/api/prisma/client.ts for detailed documentation. + */ +function getSslConfig(url: string): PoolConfig['ssl'] { + const match = url.match(/sslmode=(\w[\w-]*)/); + if (!match) return undefined; + const mode = match[1]; + switch (mode) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + default: return undefined; + } +} + function createPrismaClient(): PrismaClient { - const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + const url = process.env.DATABASE_URL!; + const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); return new PrismaClient({ adapter }); } diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index 9abf612b4f..59ab0dd8b1 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -1,10 +1,28 @@ import { PrismaClient } from '../src/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; +import type { PoolConfig } from 'pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +/** + * Derive pg SSL config from the DATABASE_URL sslmode parameter. + * See apps/api/prisma/client.ts for detailed documentation. + */ +function getSslConfig(url: string): PoolConfig['ssl'] { + const match = url.match(/sslmode=(\w[\w-]*)/); + if (!match) return undefined; + const mode = match[1]; + switch (mode) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + default: return undefined; + } +} + function createPrismaClient(): PrismaClient { - const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + const url = process.env.DATABASE_URL!; + const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); return new PrismaClient({ adapter }); } diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 0af706563c..34a948eb24 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -53,8 +53,26 @@ const clientFileContent = `import { PrismaClient } from '../src/generated/prisma import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); -export const db = globalForPrisma.prisma || new PrismaClient({ adapter }); + +function getSslConfig(url: string) { + const match = url.match(/sslmode=(\\w[\\w-]*)/); + if (!match) return undefined; + const mode = match[1]; + switch (mode) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + default: return undefined; + } +} + +function createPrismaClient(): PrismaClient { + const url = process.env.DATABASE_URL!; + const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + return new PrismaClient({ adapter }); +} + +export const db = globalForPrisma.prisma || createPrismaClient(); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; `; fs.writeFileSync(path.join(OUTPUT_DIR, 'client.ts'), clientFileContent); diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 49adb2f173..f66768121c 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -1,10 +1,31 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; +import type { PoolConfig } from 'pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +/** + * Derive pg SSL config from the DATABASE_URL sslmode parameter. + * See apps/api/prisma/client.ts for detailed documentation. + */ +function getSslConfig(url: string): PoolConfig['ssl'] { + const match = url.match(/sslmode=(\w[\w-]*)/); + if (!match) return undefined; + const mode = match[1]; + switch (mode) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + default: return undefined; + } +} -export const db = globalForPrisma.prisma || new PrismaClient({ adapter }); +function createPrismaClient(): PrismaClient { + const url = process.env.DATABASE_URL!; + const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + return new PrismaClient({ adapter }); +} + +export const db = globalForPrisma.prisma || createPrismaClient(); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; From f688334a37044d50a68d847760826e125274cc78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:08:30 -0400 Subject: [PATCH 17/31] fix: set trigger.dev runtime to node-22 (Prisma v7 requires node >=20.19 || >=22.12) (#2419) Co-authored-by: Mariano Fuentes --- apps/api/trigger.config.ts | 1 + apps/app/trigger.config.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/api/trigger.config.ts b/apps/api/trigger.config.ts index c1a40e5813..cf79b1895b 100644 --- a/apps/api/trigger.config.ts +++ b/apps/api/trigger.config.ts @@ -6,6 +6,7 @@ import { emailExtension } from './emailExtension'; import { integrationPlatformExtension } from './integrationPlatformExtension'; export default defineConfig({ + runtime: 'node-22', project: 'proj_zhioyrusqertqgafqgpj', // API project logLevel: 'log', instrumentations: [new PrismaInstrumentation()], diff --git a/apps/app/trigger.config.ts b/apps/app/trigger.config.ts index 439e6eeaa6..fb66682b30 100644 --- a/apps/app/trigger.config.ts +++ b/apps/app/trigger.config.ts @@ -5,6 +5,7 @@ import { defineConfig } from '@trigger.dev/sdk'; import { prismaExtension } from './customPrismaExtension'; export default defineConfig({ + runtime: 'node-22', project: 'proj_lhxjliiqgcdyqbgtucda', logLevel: 'log', instrumentations: [new PrismaInstrumentation()], From 00e6f13c6f22d9caee6f011783991cfe95f8f3a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:31:49 -0400 Subject: [PATCH 18/31] fix: strip sslmode from connection string before passing to pg (prevent double-parsing) (#2420) Co-authored-by: Mariano Fuentes --- apps/api/prisma/client.ts | 6 +++++- apps/app/prisma/client.ts | 4 +++- apps/framework-editor/prisma/client.ts | 4 +++- apps/portal/prisma/client.ts | 4 +++- packages/db/scripts/combine-schemas.js | 4 +++- packages/db/src/client.ts | 4 +++- 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index 7200c43c3f..f06202fb65 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -47,7 +47,11 @@ function getSslConfig(url: string): PoolConfig['ssl'] { function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + const ssl = getSslConfig(url); + // Strip sslmode from connection string — pg parses it independently and + // can override our explicit ssl config. We handle SSL entirely via the ssl option. + const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); + const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index 59ab0dd8b1..7af8b3765c 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -22,7 +22,9 @@ function getSslConfig(url: string): PoolConfig['ssl'] { function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + const ssl = getSslConfig(url); + const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); + const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index f66768121c..73f8941266 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -22,7 +22,9 @@ function getSslConfig(url: string): PoolConfig['ssl'] { function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + const ssl = getSslConfig(url); + const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); + const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index 59ab0dd8b1..7af8b3765c 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -22,7 +22,9 @@ function getSslConfig(url: string): PoolConfig['ssl'] { function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + const ssl = getSslConfig(url); + const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); + const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 34a948eb24..753b77699b 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -68,7 +68,9 @@ function getSslConfig(url: string) { function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + const ssl = getSslConfig(url); + const cleanUrl = url.replace(/[?&]sslmode=\\w[\\w-]*/g, '').replace(/\\?&/, '?').replace(/\\?$/, ''); + const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index f66768121c..73f8941266 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -22,7 +22,9 @@ function getSslConfig(url: string): PoolConfig['ssl'] { function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const adapter = new PrismaPg({ connectionString: url, ssl: getSslConfig(url) }); + const ssl = getSslConfig(url); + const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); + const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); return new PrismaClient({ adapter }); } From 8a05e29d0186c46e30f1055bd6665629ce1f01e0 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 10:41:32 -0400 Subject: [PATCH 19/31] fix(db): point prisma.config.ts to schema directory for multi-file schema support in migrations (#2422) --- packages/db/prisma.config.ts | 2 +- packages/db/prisma/schema/schema.prisma | 10 ++++++++++ packages/db/scripts/combine-schemas.js | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 packages/db/prisma/schema/schema.prisma diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts index 00f9b6fcdb..b2bb85e4f8 100644 --- a/packages/db/prisma.config.ts +++ b/packages/db/prisma.config.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ - schema: "prisma/schema.prisma", + schema: "prisma/schema", datasource: { url: process.env.DATABASE_URL!, }, diff --git a/packages/db/prisma/schema/schema.prisma b/packages/db/prisma/schema/schema.prisma new file mode 100644 index 0000000000..c02b00f006 --- /dev/null +++ b/packages/db/prisma/schema/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + extensions = [pgcrypto] +} diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 753b77699b..56ee27051e 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -17,7 +17,7 @@ let combinedSchema = fs.readFileSync(BASE_SCHEMA, 'utf8'); // Read all .prisma files from the schema directory const schemaFiles = fs .readdirSync(SCHEMA_DIR) - .filter((file) => file.endsWith('.prisma')) + .filter((file) => file.endsWith('.prisma') && file !== 'schema.prisma') .sort(); // Sort for consistent output console.log(`📁 Found ${schemaFiles.length} schema files to combine`); From 13a7b77642497d177af18e748f5ab9b98b40d86a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:51:31 -0400 Subject: [PATCH 20/31] fix(api): pin prisma@7.6.0 in Dockerfile generate step (prevents stale v6 binary resolution) (#2423) Co-authored-by: Mariano Fuentes --- apps/api/Dockerfile.multistage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index 4db3884eb5..583b33773d 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -71,7 +71,7 @@ RUN cd packages/auth && bun run build \ RUN cd packages/db && node scripts/combine-schemas.js \ && cp /app/packages/db/dist/schema.prisma /app/apps/api/prisma/schema.prisma \ && node /app/apps/api/scripts/patch-schema-generator.js \ - && cd /app/apps/api && bunx prisma generate && bunx nest build + && cd /app/apps/api && bunx prisma@7.6.0 generate && bunx nest build # ============================================================================= # STAGE 3: Production Runtime From dc9351ca705f85467c68be858b975f43fffc3f46 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 10:58:17 -0400 Subject: [PATCH 21/31] fix(api): upgrade Dockerfile base images for Prisma v7 Node.js requirement (bun 1.3.11, node 22) (#2425) --- apps/api/Dockerfile.multistage | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index 583b33773d..e585460298 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -1,7 +1,7 @@ # ============================================================================= # STAGE 1: Dependencies - Install only what the API needs # ============================================================================= -FROM oven/bun:1.2.8 AS deps +FROM oven/bun:1.3.11 AS deps WORKDIR /app @@ -35,7 +35,7 @@ RUN bun install --ignore-scripts # ============================================================================= # STAGE 2: Builder - Build workspace packages and NestJS app # ============================================================================= -FROM oven/bun:1.2.8 AS builder +FROM oven/bun:1.3.11 AS builder WORKDIR /app @@ -76,7 +76,7 @@ RUN cd packages/db && node scripts/combine-schemas.js \ # ============================================================================= # STAGE 3: Production Runtime # ============================================================================= -FROM node:20-slim AS production +FROM node:22-slim AS production # Create non-root user before copying files so COPY --chown can use it RUN groupadd --system nestjs && useradd --system --gid nestjs --create-home nestjs From a98cf939610f50c9d564f6d551aff7db3313fe63 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 11:01:35 -0400 Subject: [PATCH 22/31] fix(db): remove dotenv/config import from prisma.config.ts (not available in Docker build context) (#2426) --- packages/db/prisma.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts index b2bb85e4f8..6e980e4651 100644 --- a/packages/db/prisma.config.ts +++ b/packages/db/prisma.config.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ From fab6693b5e0d900472fa06a5f7c8978107ec7785 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 11:06:09 -0400 Subject: [PATCH 23/31] fix: use installed prisma binary instead of bunx (fixes prisma/config resolution in Docker) (#2427) --- apps/api/Dockerfile.multistage | 2 +- packages/db/scripts/generate-prisma-client-js.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index e585460298..b6a50ca1a9 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -71,7 +71,7 @@ RUN cd packages/auth && bun run build \ RUN cd packages/db && node scripts/combine-schemas.js \ && cp /app/packages/db/dist/schema.prisma /app/apps/api/prisma/schema.prisma \ && node /app/apps/api/scripts/patch-schema-generator.js \ - && cd /app/apps/api && bunx prisma@7.6.0 generate && bunx nest build + && cd /app/apps/api && /app/node_modules/.bin/prisma generate && bunx nest build # ============================================================================= # STAGE 3: Production Runtime diff --git a/packages/db/scripts/generate-prisma-client-js.js b/packages/db/scripts/generate-prisma-client-js.js index 28f834df11..07a323adb0 100644 --- a/packages/db/scripts/generate-prisma-client-js.js +++ b/packages/db/scripts/generate-prisma-client-js.js @@ -24,8 +24,21 @@ schema = schema fs.writeFileSync(tempSchema, schema); +// Resolve the installed prisma binary instead of using bunx (which downloads +// to a temp dir and can't resolve prisma/config from prisma.config.ts). +function findPrismaBin() { + const candidates = [ + path.join(__dirname, '../node_modules/.bin/prisma'), + path.join(__dirname, '../../../node_modules/.bin/prisma'), + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return candidate; + } + return 'prisma'; // fallback to PATH +} + try { - execFileSync('bunx', ['prisma', 'generate', `--schema=${tempSchema}`], { stdio: 'inherit' }); + execFileSync(findPrismaBin(), ['generate', `--schema=${tempSchema}`], { stdio: 'inherit' }); } finally { fs.unlinkSync(tempSchema); } From c58045f214e268fddf6d1d7fb142eb821111c4d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:15:05 -0400 Subject: [PATCH 24/31] refactor: simplify prisma schema sharing using native v7 multi-file schemas * refactor: simplify prisma schema sharing using native v7 multi-file schemas Replace combine-schemas.js-based pipeline with Prisma v7's native directory support. Each app now has its own prisma/schema/ dir with a local schema.prisma (committed) and copies model files from packages/db/prisma/schema/ at generate time (gitignored). Co-Authored-By: Claude Sonnet 4.6 * refactor: simplify prisma schema sharing and fix Docker build - Use native Prisma v7 multi-file schema (--schema=prisma/schema directory) - Each app has its own prisma/schema/schema.prisma with correct generator - generate-prisma-client-js.js creates temp dir with model files instead of combine-schemas - Remove PoolConfig import from pg (not a direct dependency) - Keep bun:1.2.8 for builder (compatible with workspace resolution), node:22 for production - Restore original COPY order in Dockerfile (no node_modules restore needed) --------- Co-authored-by: Mariano Fuentes Co-authored-by: Claude Sonnet 4.6 --- .../trigger-api-tasks-deploy-main.yml | 8 +-- .../trigger-api-tasks-deploy-release.yml | 8 +-- apps/api/.gitignore | 7 ++- apps/api/Dockerfile.multistage | 17 +++--- apps/api/package.json | 6 +- apps/api/prisma.config.ts | 9 --- apps/api/prisma/client.ts | 4 +- apps/api/prisma/schema/schema.prisma | 9 +++ apps/api/scripts/patch-schema-generator.js | 15 ----- apps/app/.gitignore | 5 +- apps/app/package.json | 8 +-- apps/app/prisma.config.ts | 2 +- apps/app/prisma/client.ts | 4 +- apps/app/prisma/schema/schema.prisma | 10 +++ apps/framework-editor/.gitignore | 3 + apps/framework-editor/package.json | 2 +- apps/framework-editor/prisma.config.ts | 2 +- apps/framework-editor/prisma/client.ts | 4 +- .../prisma/schema/schema.prisma | 10 +++ apps/portal/.gitignore | 5 +- apps/portal/package.json | 6 +- apps/portal/prisma.config.ts | 2 +- apps/portal/prisma/client.ts | 4 +- apps/portal/prisma/schema/schema.prisma | 10 +++ packages/db/package.json | 4 +- .../db/scripts/generate-prisma-client-js.js | 61 ++++++++++--------- packages/db/src/client.ts | 4 +- 27 files changed, 125 insertions(+), 104 deletions(-) delete mode 100644 apps/api/prisma.config.ts create mode 100644 apps/api/prisma/schema/schema.prisma delete mode 100644 apps/api/scripts/patch-schema-generator.js create mode 100644 apps/app/prisma/schema/schema.prisma create mode 100644 apps/framework-editor/prisma/schema/schema.prisma create mode 100644 apps/portal/prisma/schema/schema.prisma diff --git a/.github/workflows/trigger-api-tasks-deploy-main.yml b/.github/workflows/trigger-api-tasks-deploy-main.yml index 6842f036a1..2c9db0cb3a 100644 --- a/.github/workflows/trigger-api-tasks-deploy-main.yml +++ b/.github/workflows/trigger-api-tasks-deploy-main.yml @@ -37,13 +37,11 @@ jobs: - name: Build DB package working-directory: ./packages/db run: bun run build - - name: Copy schema to api and generate client + - name: Copy model files to api schema dir and generate client working-directory: ./apps/api run: | - mkdir -p prisma - cp ../../packages/db/dist/schema.prisma prisma/schema.prisma - node scripts/patch-schema-generator.js - bunx prisma generate + find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \; + bunx prisma generate --schema=prisma/schema - name: 🚀 Deploy Trigger.dev working-directory: ./apps/api timeout-minutes: 20 diff --git a/.github/workflows/trigger-api-tasks-deploy-release.yml b/.github/workflows/trigger-api-tasks-deploy-release.yml index fa3912797e..86d87cb52f 100644 --- a/.github/workflows/trigger-api-tasks-deploy-release.yml +++ b/.github/workflows/trigger-api-tasks-deploy-release.yml @@ -43,13 +43,11 @@ jobs: working-directory: ./packages/db run: bun run build - - name: Copy schema to api and generate client + - name: Copy model files to api schema dir and generate client working-directory: ./apps/api run: | - mkdir -p prisma - cp ../../packages/db/dist/schema.prisma prisma/schema.prisma - node scripts/patch-schema-generator.js - bunx prisma generate + find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \; + bunx prisma generate --schema=prisma/schema - name: 🚀 Deploy Trigger.dev working-directory: ./apps/api diff --git a/apps/api/.gitignore b/apps/api/.gitignore index f0d96682de..8ba6979bb4 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -47,9 +47,8 @@ lerna-debug.log* .env.production.local .env.local -# Local scripts (tracked scripts are explicitly excluded below) +# Local scripts scripts/ -!scripts/patch-schema-generator.js # temp directory .temp @@ -65,6 +64,8 @@ pids report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json -prisma/schema.prisma +# Model files are copied here by db:getschema — only schema.prisma is committed +prisma/schema/*.prisma +!prisma/schema/schema.prisma trigger.config.js test/**/*.js diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index b6a50ca1a9..38269410d2 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -1,7 +1,7 @@ # ============================================================================= # STAGE 1: Dependencies - Install only what the API needs # ============================================================================= -FROM oven/bun:1.3.11 AS deps +FROM oven/bun:1.2.8 AS deps WORKDIR /app @@ -35,7 +35,7 @@ RUN bun install --ignore-scripts # ============================================================================= # STAGE 2: Builder - Build workspace packages and NestJS app # ============================================================================= -FROM oven/bun:1.3.11 AS builder +FROM oven/bun:1.2.8 AS builder WORKDIR /app @@ -46,7 +46,8 @@ COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/package.json ./package.json COPY --from=deps /app/bun.lock ./bun.lock -# Copy workspace packages source +# Copy workspace packages source (node_modules excluded by .dockerignore, +# but COPY replaces directories so workspace node_modules from deps get wiped) COPY packages/auth ./packages/auth COPY packages/db ./packages/db COPY packages/utils ./packages/utils @@ -58,7 +59,7 @@ COPY packages/company ./packages/company # Copy API source COPY apps/api ./apps/api -# Build db first — generates Prisma client needed by other packages +# Build db — generates Prisma client + compiles dist/ for @trycompai/db consumers RUN cd packages/db && bun run build # Build remaining workspace packages @@ -67,11 +68,9 @@ RUN cd packages/auth && bun run build \ && cd ../email && bun run build \ && cd ../company && bun run build -# Generate Prisma schema for API and build NestJS app -RUN cd packages/db && node scripts/combine-schemas.js \ - && cp /app/packages/db/dist/schema.prisma /app/apps/api/prisma/schema.prisma \ - && node /app/apps/api/scripts/patch-schema-generator.js \ - && cd /app/apps/api && /app/node_modules/.bin/prisma generate && bunx nest build +# Copy model files to api schema dir and generate Prisma client, then build NestJS app +RUN find /app/packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} /app/apps/api/prisma/schema/ \; \ + && cd /app/apps/api && /app/node_modules/.bin/prisma generate --schema=prisma/schema && bunx nest build # ============================================================================= # STAGE 3: Production Runtime diff --git a/apps/api/package.json b/apps/api/package.json index c5332ca2ca..23f23bfa68 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -122,9 +122,9 @@ "private": true, "scripts": { "build": "nest build", - "build:docker": "bunx prisma generate && nest build", - "db:generate": "bun run db:getschema && bunx prisma generate", - "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma && node scripts/patch-schema-generator.js", + "build:docker": "bunx prisma generate --schema=prisma/schema && nest build", + "db:generate": "bun run db:getschema && bunx prisma generate --schema=prisma/schema", + "db:getschema": "find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \\;", "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/api", "deploy:trigger-prod": "npx trigger.dev@4.4.3 deploy", "dev": "bunx concurrently --kill-others --names \"nest,trigger\" --prefix-colors \"green,blue\" \"nest start --watch\" \"trigger dev\"", diff --git a/apps/api/prisma.config.ts b/apps/api/prisma.config.ts deleted file mode 100644 index a02b89f336..0000000000 --- a/apps/api/prisma.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import "dotenv/config"; -import { defineConfig } from "prisma/config"; - -export default defineConfig({ - schema: "prisma/schema.prisma", - datasource: { - url: process.env.DATABASE_URL!, - }, -}); diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index f06202fb65..ea6f93d902 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -1,6 +1,6 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -import type { PoolConfig } from 'pg'; + const globalForPrisma = global as unknown as { prisma: PrismaClient }; @@ -26,7 +26,7 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; * root CA store, so rejectUnauthorized: false is required. The connection * is still TLS-encrypted — only identity verification is skipped. */ -function getSslConfig(url: string): PoolConfig['ssl'] { +function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { const match = url.match(/sslmode=(\w[\w-]*)/); if (!match) return undefined; diff --git a/apps/api/prisma/schema/schema.prisma b/apps/api/prisma/schema/schema.prisma new file mode 100644 index 0000000000..551e0884a4 --- /dev/null +++ b/apps/api/prisma/schema/schema.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + extensions = [pgcrypto] +} diff --git a/apps/api/scripts/patch-schema-generator.js b/apps/api/scripts/patch-schema-generator.js deleted file mode 100644 index 44580544ed..0000000000 --- a/apps/api/scripts/patch-schema-generator.js +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs'); -const path = require('path'); - -const schemaPath = path.join(__dirname, '../prisma/schema.prisma'); -let schema = fs.readFileSync(schemaPath, 'utf8'); - -// Surgically patch the generator block: swap provider and remove output line. -// Preserves all other generator settings (previewFeatures, etc.). -schema = schema - .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') - .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); - -fs.writeFileSync(schemaPath, schema); -console.log('[patch-schema] Set generator to prisma-client-js (default output)'); diff --git a/apps/app/.gitignore b/apps/app/.gitignore index a9e6967739..3ea39de1ef 100644 --- a/apps/app/.gitignore +++ b/apps/app/.gitignore @@ -53,5 +53,6 @@ next-env.d.ts # Generated Prisma Client prisma/generated src/generated/ -# Copied schema from @trycompai/db package - always generate fresh -prisma/schema.prisma +# Model files are copied here by db:getschema — only schema.prisma is committed +prisma/schema/*.prisma +!prisma/schema/schema.prisma diff --git a/apps/app/package.json b/apps/app/package.json index a3d4f14d18..82d8bbd5f2 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -186,15 +186,15 @@ "scripts": { "analyze-locale-usage": "bunx tsx src/locales/analyze-locale-usage.ts", "build": "next build", - "build:docker": "prisma generate && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma && next build", - "db:generate": "bun run db:getschema && prisma generate && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", - "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", + "build:docker": "prisma generate --schema=prisma/schema && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma && next build", + "db:generate": "bun run db:getschema && prisma generate --schema=prisma/schema && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", + "db:getschema": "find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \\;", "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/app", "deploy:trigger-prod": "npx trigger.dev@4.4.3 deploy", "dev": "bun i && bunx concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"NODE_OPTIONS='--no-deprecation' next dev --turbo -p 3000\" \"NODE_OPTIONS='--no-deprecation' trigger dev\"", "lint": "eslint . && prettier --check .", "prebuild": "bun run db:generate", - "postinstall": "prisma generate --schema=./prisma/schema.prisma || exit 0", + "postinstall": "prisma generate --schema=./prisma/schema || exit 0", "start": "next start", "test": "vitest", "test:all": "./scripts/test-all.sh", diff --git a/apps/app/prisma.config.ts b/apps/app/prisma.config.ts index a02b89f336..c0e1147ed5 100644 --- a/apps/app/prisma.config.ts +++ b/apps/app/prisma.config.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ - schema: "prisma/schema.prisma", + schema: "prisma/schema", datasource: { url: process.env.DATABASE_URL!, }, diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index 7af8b3765c..bb300b920b 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -1,6 +1,6 @@ import { PrismaClient } from '../src/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -import type { PoolConfig } from 'pg'; + const globalForPrisma = global as unknown as { prisma: PrismaClient }; @@ -8,7 +8,7 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; * Derive pg SSL config from the DATABASE_URL sslmode parameter. * See apps/api/prisma/client.ts for detailed documentation. */ -function getSslConfig(url: string): PoolConfig['ssl'] { +function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { const match = url.match(/sslmode=(\w[\w-]*)/); if (!match) return undefined; const mode = match[1]; diff --git a/apps/app/prisma/schema/schema.prisma b/apps/app/prisma/schema/schema.prisma new file mode 100644 index 0000000000..45f4244cf6 --- /dev/null +++ b/apps/app/prisma/schema/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client" + output = "../../src/generated/prisma" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + extensions = [pgcrypto] +} diff --git a/apps/framework-editor/.gitignore b/apps/framework-editor/.gitignore index d3db8f9b18..10d8192ae9 100644 --- a/apps/framework-editor/.gitignore +++ b/apps/framework-editor/.gitignore @@ -1 +1,4 @@ src/generated/ +# Model files are copied here by db:generate — only schema.prisma is committed +prisma/schema/*.prisma +!prisma/schema/schema.prisma diff --git a/apps/framework-editor/package.json b/apps/framework-editor/package.json index 90e5f35b8b..f0f5a9b2a1 100644 --- a/apps/framework-editor/package.json +++ b/apps/framework-editor/package.json @@ -42,7 +42,7 @@ "private": true, "scripts": { "build": "next build", - "db:generate": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma && prisma generate", + "db:generate": "find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \\; && prisma generate --schema=prisma/schema && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", "dev": "next dev --port 3004", "lint": "echo 'no lint configured'", "prebuild": "bun run db:generate", diff --git a/apps/framework-editor/prisma.config.ts b/apps/framework-editor/prisma.config.ts index a02b89f336..c0e1147ed5 100644 --- a/apps/framework-editor/prisma.config.ts +++ b/apps/framework-editor/prisma.config.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ - schema: "prisma/schema.prisma", + schema: "prisma/schema", datasource: { url: process.env.DATABASE_URL!, }, diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index 73f8941266..350cec96a2 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -1,6 +1,6 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -import type { PoolConfig } from 'pg'; + const globalForPrisma = global as unknown as { prisma: PrismaClient }; @@ -8,7 +8,7 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; * Derive pg SSL config from the DATABASE_URL sslmode parameter. * See apps/api/prisma/client.ts for detailed documentation. */ -function getSslConfig(url: string): PoolConfig['ssl'] { +function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { const match = url.match(/sslmode=(\w[\w-]*)/); if (!match) return undefined; const mode = match[1]; diff --git a/apps/framework-editor/prisma/schema/schema.prisma b/apps/framework-editor/prisma/schema/schema.prisma new file mode 100644 index 0000000000..45f4244cf6 --- /dev/null +++ b/apps/framework-editor/prisma/schema/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client" + output = "../../src/generated/prisma" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + extensions = [pgcrypto] +} diff --git a/apps/portal/.gitignore b/apps/portal/.gitignore index f2717bc8f5..abb188d6ba 100644 --- a/apps/portal/.gitignore +++ b/apps/portal/.gitignore @@ -43,5 +43,6 @@ next-env.d.ts # Generated Prisma Client prisma/generated src/generated/ -# Copied schema from @trycompai/db package - always generate fresh -prisma/schema.prisma +# Model files are copied here by db:getschema — only schema.prisma is committed +prisma/schema/*.prisma +!prisma/schema/schema.prisma diff --git a/apps/portal/package.json b/apps/portal/package.json index 716a2e722b..666553369f 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -54,9 +54,9 @@ "private": true, "scripts": { "build": "next build", - "build:docker": "prisma generate && next build", - "db:generate": "bun run db:getschema && prisma generate && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", - "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", + "build:docker": "prisma generate --schema=prisma/schema && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma && next build", + "db:generate": "bun run db:getschema && prisma generate --schema=prisma/schema && node ../../packages/db/scripts/fix-generated-extensions.js src/generated/prisma", + "db:getschema": "find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \\;", "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/portal", "dev": "next dev --turbopack -p 3002", "lint": "eslint . && prettier --check .", diff --git a/apps/portal/prisma.config.ts b/apps/portal/prisma.config.ts index a02b89f336..c0e1147ed5 100644 --- a/apps/portal/prisma.config.ts +++ b/apps/portal/prisma.config.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ - schema: "prisma/schema.prisma", + schema: "prisma/schema", datasource: { url: process.env.DATABASE_URL!, }, diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index 7af8b3765c..bb300b920b 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -1,6 +1,6 @@ import { PrismaClient } from '../src/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -import type { PoolConfig } from 'pg'; + const globalForPrisma = global as unknown as { prisma: PrismaClient }; @@ -8,7 +8,7 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; * Derive pg SSL config from the DATABASE_URL sslmode parameter. * See apps/api/prisma/client.ts for detailed documentation. */ -function getSslConfig(url: string): PoolConfig['ssl'] { +function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { const match = url.match(/sslmode=(\w[\w-]*)/); if (!match) return undefined; const mode = match[1]; diff --git a/apps/portal/prisma/schema/schema.prisma b/apps/portal/prisma/schema/schema.prisma new file mode 100644 index 0000000000..45f4244cf6 --- /dev/null +++ b/apps/portal/prisma/schema/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client" + output = "../../src/generated/prisma" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + extensions = [pgcrypto] +} diff --git a/packages/db/package.json b/packages/db/package.json index 6a3b319e11..23a99b8bfd 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -43,9 +43,9 @@ "directory": "packages/db" }, "scripts": { - "build": "rm -rf dist && node scripts/combine-schemas.js && node scripts/generate-prisma-client-js.js && tsc", + "build": "rm -rf dist && node scripts/generate-prisma-client-js.js && tsc", "check-types": "tsc --noEmit", - "db:generate": "node scripts/combine-schemas.js && node scripts/generate-prisma-client-js.js", + "db:generate": "node scripts/generate-prisma-client-js.js", "db:migrate": "prisma migrate dev", "db:migrate:reset": "prisma migrate reset", "db:push": "prisma db push", diff --git a/packages/db/scripts/generate-prisma-client-js.js b/packages/db/scripts/generate-prisma-client-js.js index 07a323adb0..28892e9304 100644 --- a/packages/db/scripts/generate-prisma-client-js.js +++ b/packages/db/scripts/generate-prisma-client-js.js @@ -1,44 +1,49 @@ #!/usr/bin/env node /** * Generates a prisma-client-js client to populate @prisma/client at node_modules. - * This is fast (no .ts files to compile) and provides runtime enums + types - * for packages that import from @trycompai/db. - * - * The combined schema uses prisma-client provider. This script patches a temp copy - * to use prisma-client-js (no custom output) and generates to node_modules/@prisma/client. + * Creates a temp schema dir, copies model files, generates with prisma-client-js. */ const { execFileSync } = require('child_process'); const fs = require('fs'); const path = require('path'); -const schemaPath = path.join(__dirname, '../dist/schema.prisma'); -const tempSchema = path.join(__dirname, '../dist/.schema-clientjs.prisma'); +const root = path.join(__dirname, '..'); +const schemaDir = path.join(root, 'prisma/schema'); +const configPath = path.join(root, 'prisma.config.ts'); +const configBackup = configPath + '.bak'; +const tempDir = fs.mkdtempSync(path.join(root, '.prisma-clientjs-')); -let schema = fs.readFileSync(schemaPath, 'utf8'); +// Hide prisma.config.ts so prisma doesn't try to load it +const hasConfig = fs.existsSync(configPath); +if (hasConfig) fs.renameSync(configPath, configBackup); -// Surgically patch: swap provider and remove output line. -// Preserves all other generator settings (previewFeatures, etc.). -schema = schema - .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') - .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); +try { + // Copy model files + for (const file of fs.readdirSync(schemaDir).filter(f => f.endsWith('.prisma') && f !== 'schema.prisma')) { + fs.copyFileSync(path.join(schemaDir, file), path.join(tempDir, file)); + } -fs.writeFileSync(tempSchema, schema); + // Write prisma-client-js generator schema + fs.writeFileSync(path.join(tempDir, 'schema.prisma'), `generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} -// Resolve the installed prisma binary instead of using bunx (which downloads -// to a temp dir and can't resolve prisma/config from prisma.config.ts). -function findPrismaBin() { - const candidates = [ - path.join(__dirname, '../node_modules/.bin/prisma'), - path.join(__dirname, '../../../node_modules/.bin/prisma'), - ]; - for (const candidate of candidates) { - if (fs.existsSync(candidate)) return candidate; - } - return 'prisma'; // fallback to PATH +datasource db { + provider = "postgresql" + extensions = [pgcrypto] } +`); -try { - execFileSync(findPrismaBin(), ['generate', `--schema=${tempSchema}`], { stdio: 'inherit' }); + // Resolve prisma CLI via Node/Bun module resolution (handles hoisting) + const prismaPackage = require.resolve('prisma/package.json'); + const prismaCli = path.join(path.dirname(prismaPackage), 'build', 'index.js'); + + execFileSync(process.execPath, [prismaCli, 'generate', `--schema=${tempDir}`], { + stdio: 'inherit', + cwd: root, + }); } finally { - fs.unlinkSync(tempSchema); + fs.rmSync(tempDir, { recursive: true, force: true }); + if (hasConfig && fs.existsSync(configBackup)) fs.renameSync(configBackup, configPath); } diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 73f8941266..350cec96a2 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -1,6 +1,6 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -import type { PoolConfig } from 'pg'; + const globalForPrisma = global as unknown as { prisma: PrismaClient }; @@ -8,7 +8,7 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; * Derive pg SSL config from the DATABASE_URL sslmode parameter. * See apps/api/prisma/client.ts for detailed documentation. */ -function getSslConfig(url: string): PoolConfig['ssl'] { +function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { const match = url.match(/sslmode=(\w[\w-]*)/); if (!match) return undefined; const mode = match[1]; From 98213f81f371945ff385bb25ad17d5aaaef82e04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:29:51 -0400 Subject: [PATCH 25/31] fix: default to SSL for non-localhost connections, remove buggy cleanUrl stripping (#2430) Co-authored-by: Mariano Fuentes --- apps/api/prisma/client.ts | 64 +++++++++++--------------- apps/app/prisma/client.ts | 25 ++++------ apps/framework-editor/prisma/client.ts | 25 ++++------ apps/portal/prisma/client.ts | 25 ++++------ packages/db/scripts/combine-schemas.js | 20 ++++---- packages/db/src/client.ts | 25 ++++------ 6 files changed, 78 insertions(+), 106 deletions(-) diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index ea6f93d902..8110cb715f 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -1,57 +1,49 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; - const globalForPrisma = global as unknown as { prisma: PrismaClient }; /** - * Derive pg SSL config from the DATABASE_URL sslmode parameter. - * - * pg-connection-string parses sslmode=require into ssl: {} (empty object), - * and pg@8+ treats ssl: {} as { rejectUnauthorized: true }, which rejects - * AWS RDS Proxy's self-signed certificate. This is a known pg@8 breaking - * change (https://node-postgres.com/announcements#ssl-by-default). + * Derive pg SSL config from the DATABASE_URL. * - * Per PostgreSQL spec (https://www.postgresql.org/docs/current/libpq-ssl.html): - * - require: encrypt connection, skip certificate verification - * - verify-ca: encrypt + verify server CA - * - verify-full: encrypt + verify CA + verify hostname + * pg@8+ defaults rejectUnauthorized to true, which rejects AWS RDS Proxy's + * certificate (signed by internal AWS CA, not in Node.js root CA store). * - * Per Prisma v7 migration docs: "SSL certificate defaults have changed. - * Previously [v6 Rust engine], invalid SSL certificates were ignored." - * (https://www.prisma.io/docs/orm/more/upgrades/to-v7) + * Per PostgreSQL sslmode spec: + * - disable: no SSL + * - require: encrypt, skip certificate verification + * - verify-ca: encrypt + verify CA + * - verify-full: encrypt + verify CA + hostname * - * Our infra enforces sslmode=require with RDS Proxy (requireTls: true). - * The proxy's certificate is signed by an internal AWS CA not in Node.js's - * root CA store, so rejectUnauthorized: false is required. The connection - * is still TLS-encrypted — only identity verification is skipped. + * When no sslmode is set, we default to SSL with rejectUnauthorized: false + * for non-localhost connections (matches Prisma v6 behavior where the Rust + * engine silently accepted all certificates). */ function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const match = url.match(/sslmode=(\w[\w-]*)/); - if (!match) return undefined; + const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); - const mode = match[1]; - switch (mode) { - case 'disable': - return undefined; - case 'require': - case 'no-verify': - return { rejectUnauthorized: false }; - case 'verify-ca': - case 'verify-full': - return { rejectUnauthorized: true }; - default: - return undefined; + if (sslmodeMatch) { + switch (sslmodeMatch[1]) { + case 'disable': + return undefined; + case 'require': + case 'no-verify': + return { rejectUnauthorized: false }; + case 'verify-ca': + case 'verify-full': + return { rejectUnauthorized: true }; + } } + + // No sslmode specified — enable SSL for non-localhost (production default) + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + return isLocalhost ? undefined : { rejectUnauthorized: false }; } function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const ssl = getSslConfig(url); - // Strip sslmode from connection string — pg parses it independently and - // can override our explicit ssl config. We handle SSL entirely via the ssl option. - const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); - const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index bb300b920b..544dc15d77 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -1,30 +1,25 @@ import { PrismaClient } from '../src/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; - const globalForPrisma = global as unknown as { prisma: PrismaClient }; -/** - * Derive pg SSL config from the DATABASE_URL sslmode parameter. - * See apps/api/prisma/client.ts for detailed documentation. - */ function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const match = url.match(/sslmode=(\w[\w-]*)/); - if (!match) return undefined; - const mode = match[1]; - switch (mode) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - default: return undefined; + const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); + if (sslmodeMatch) { + switch (sslmodeMatch[1]) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + } } + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + return isLocalhost ? undefined : { rejectUnauthorized: false }; } function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const ssl = getSslConfig(url); - const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); - const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index 350cec96a2..a583419d89 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -1,30 +1,25 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; - const globalForPrisma = global as unknown as { prisma: PrismaClient }; -/** - * Derive pg SSL config from the DATABASE_URL sslmode parameter. - * See apps/api/prisma/client.ts for detailed documentation. - */ function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const match = url.match(/sslmode=(\w[\w-]*)/); - if (!match) return undefined; - const mode = match[1]; - switch (mode) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - default: return undefined; + const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); + if (sslmodeMatch) { + switch (sslmodeMatch[1]) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + } } + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + return isLocalhost ? undefined : { rejectUnauthorized: false }; } function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const ssl = getSslConfig(url); - const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); - const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index bb300b920b..544dc15d77 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -1,30 +1,25 @@ import { PrismaClient } from '../src/generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; - const globalForPrisma = global as unknown as { prisma: PrismaClient }; -/** - * Derive pg SSL config from the DATABASE_URL sslmode parameter. - * See apps/api/prisma/client.ts for detailed documentation. - */ function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const match = url.match(/sslmode=(\w[\w-]*)/); - if (!match) return undefined; - const mode = match[1]; - switch (mode) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - default: return undefined; + const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); + if (sslmodeMatch) { + switch (sslmodeMatch[1]) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + } } + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + return isLocalhost ? undefined : { rejectUnauthorized: false }; } function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const ssl = getSslConfig(url); - const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); - const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 56ee27051e..5e5552ad76 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -55,22 +55,22 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; function getSslConfig(url: string) { - const match = url.match(/sslmode=(\\w[\\w-]*)/); - if (!match) return undefined; - const mode = match[1]; - switch (mode) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - default: return undefined; + const sslmodeMatch = url.match(/sslmode=(\\w[\\w-]*)/); + if (sslmodeMatch) { + switch (sslmodeMatch[1]) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + } } + const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(url); + return isLocalhost ? undefined : { rejectUnauthorized: false }; } function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const ssl = getSslConfig(url); - const cleanUrl = url.replace(/[?&]sslmode=\\w[\\w-]*/g, '').replace(/\\?&/, '?').replace(/\\?$/, ''); - const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 350cec96a2..a583419d89 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -1,30 +1,25 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; - const globalForPrisma = global as unknown as { prisma: PrismaClient }; -/** - * Derive pg SSL config from the DATABASE_URL sslmode parameter. - * See apps/api/prisma/client.ts for detailed documentation. - */ function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const match = url.match(/sslmode=(\w[\w-]*)/); - if (!match) return undefined; - const mode = match[1]; - switch (mode) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - default: return undefined; + const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); + if (sslmodeMatch) { + switch (sslmodeMatch[1]) { + case 'disable': return undefined; + case 'require': case 'no-verify': return { rejectUnauthorized: false }; + case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; + } } + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + return isLocalhost ? undefined : { rejectUnauthorized: false }; } function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const ssl = getSslConfig(url); - const cleanUrl = url.replace(/[?&]sslmode=\w[\w-]*/g, '').replace(/\?&/, '?').replace(/\?$/, ''); - const adapter = new PrismaPg({ connectionString: cleanUrl, ssl }); + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } From 863f14be9ba0c3c418196026662c9b6de530e60b Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 12:45:14 -0400 Subject: [PATCH 26/31] fix: use AWS RDS CA bundle for proper SSL verification, simplify client SSL config (#2432) --- apps/api/Dockerfile.multistage | 7 +++-- apps/api/prisma/client.ts | 41 ++------------------------ apps/app/prisma/client.ts | 17 ++--------- apps/framework-editor/prisma/client.ts | 17 ++--------- apps/portal/prisma/client.ts | 17 ++--------- packages/db/scripts/combine-schemas.js | 17 ++--------- packages/db/src/client.ts | 17 ++--------- 7 files changed, 17 insertions(+), 116 deletions(-) diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index 38269410d2..671ec2cfe3 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -83,8 +83,11 @@ RUN groupadd --system nestjs && useradd --system --gid nestjs --create-home nest WORKDIR /app RUN chown nestjs:nestjs /app -# Install runtime dependencies -RUN apt-get update && apt-get install -y --no-install-recommends wget && rm -rf /var/lib/apt/lists/* +# Install runtime dependencies + AWS RDS CA certificate bundle +RUN apt-get update && apt-get install -y --no-install-recommends wget && rm -rf /var/lib/apt/lists/* \ + && wget -q -O /usr/local/share/aws-rds-ca-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem + +ENV NODE_EXTRA_CA_CERTS=/usr/local/share/aws-rds-ca-bundle.pem # Copy built NestJS app COPY --from=builder --chown=nestjs:nestjs /app/apps/api/dist ./dist diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index 8110cb715f..efa583aeb7 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -3,47 +3,10 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -/** - * Derive pg SSL config from the DATABASE_URL. - * - * pg@8+ defaults rejectUnauthorized to true, which rejects AWS RDS Proxy's - * certificate (signed by internal AWS CA, not in Node.js root CA store). - * - * Per PostgreSQL sslmode spec: - * - disable: no SSL - * - require: encrypt, skip certificate verification - * - verify-ca: encrypt + verify CA - * - verify-full: encrypt + verify CA + hostname - * - * When no sslmode is set, we default to SSL with rejectUnauthorized: false - * for non-localhost connections (matches Prisma v6 behavior where the Rust - * engine silently accepted all certificates). - */ -function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); - - if (sslmodeMatch) { - switch (sslmodeMatch[1]) { - case 'disable': - return undefined; - case 'require': - case 'no-verify': - return { rejectUnauthorized: false }; - case 'verify-ca': - case 'verify-full': - return { rejectUnauthorized: true }; - } - } - - // No sslmode specified — enable SSL for non-localhost (production default) - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - return isLocalhost ? undefined : { rejectUnauthorized: false }; -} - function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const ssl = getSslConfig(url); - const adapter = new PrismaPg({ connectionString: url, ssl }); + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); return new PrismaClient({ adapter }); } diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index 544dc15d77..4630094945 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -3,23 +3,10 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); - if (sslmodeMatch) { - switch (sslmodeMatch[1]) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - } - } - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - return isLocalhost ? undefined : { rejectUnauthorized: false }; -} - function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const ssl = getSslConfig(url); - const adapter = new PrismaPg({ connectionString: url, ssl }); + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); return new PrismaClient({ adapter }); } diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index a583419d89..efa583aeb7 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -3,23 +3,10 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); - if (sslmodeMatch) { - switch (sslmodeMatch[1]) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - } - } - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - return isLocalhost ? undefined : { rejectUnauthorized: false }; -} - function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const ssl = getSslConfig(url); - const adapter = new PrismaPg({ connectionString: url, ssl }); + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); return new PrismaClient({ adapter }); } diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index 544dc15d77..4630094945 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -3,23 +3,10 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); - if (sslmodeMatch) { - switch (sslmodeMatch[1]) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - } - } - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - return isLocalhost ? undefined : { rejectUnauthorized: false }; -} - function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const ssl = getSslConfig(url); - const adapter = new PrismaPg({ connectionString: url, ssl }); + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); return new PrismaClient({ adapter }); } diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 5e5552ad76..2c68ebde67 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -54,23 +54,10 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -function getSslConfig(url: string) { - const sslmodeMatch = url.match(/sslmode=(\\w[\\w-]*)/); - if (sslmodeMatch) { - switch (sslmodeMatch[1]) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - } - } - const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(url); - return isLocalhost ? undefined : { rejectUnauthorized: false }; -} - function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const ssl = getSslConfig(url); - const adapter = new PrismaPg({ connectionString: url, ssl }); + const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(url); + const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); return new PrismaClient({ adapter }); } diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index a583419d89..efa583aeb7 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -3,23 +3,10 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -function getSslConfig(url: string): boolean | { rejectUnauthorized: boolean } | undefined { - const sslmodeMatch = url.match(/sslmode=(\w[\w-]*)/); - if (sslmodeMatch) { - switch (sslmodeMatch[1]) { - case 'disable': return undefined; - case 'require': case 'no-verify': return { rejectUnauthorized: false }; - case 'verify-ca': case 'verify-full': return { rejectUnauthorized: true }; - } - } - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - return isLocalhost ? undefined : { rejectUnauthorized: false }; -} - function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; - const ssl = getSslConfig(url); - const adapter = new PrismaPg({ connectionString: url, ssl }); + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); return new PrismaClient({ adapter }); } From 772ac4865a634d898c0fe908a54b9bbaeea5aa49 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 12:47:52 -0400 Subject: [PATCH 27/31] fix: install ca-certificates before wget, clean apt after download (#2433) --- apps/api/Dockerfile.multistage | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index 671ec2cfe3..045cf48834 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -84,8 +84,9 @@ WORKDIR /app RUN chown nestjs:nestjs /app # Install runtime dependencies + AWS RDS CA certificate bundle -RUN apt-get update && apt-get install -y --no-install-recommends wget && rm -rf /var/lib/apt/lists/* \ - && wget -q -O /usr/local/share/aws-rds-ca-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem +RUN apt-get update && apt-get install -y --no-install-recommends wget ca-certificates \ + && wget -q -O /usr/local/share/aws-rds-ca-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ + && apt-get clean && rm -rf /var/lib/apt/lists/* ENV NODE_EXTRA_CA_CERTS=/usr/local/share/aws-rds-ca-bundle.pem From b7b79446654ef9a79930f33bc042fe015690726a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:54:29 -0400 Subject: [PATCH 28/31] fix: install ca-certificates before wget, clean apt after download (#2434) Co-authored-by: Mariano Fuentes --- .github/workflows/trigger-tasks-deploy-main.yml | 6 +++--- .github/workflows/trigger-tasks-deploy-release.yml | 6 +++--- apps/api/prisma/client.ts | 6 +++++- apps/app/prisma/client.ts | 6 +++++- apps/framework-editor/prisma/client.ts | 6 +++++- apps/portal/prisma/client.ts | 6 +++++- packages/db/scripts/combine-schemas.js | 4 +++- packages/db/src/client.ts | 6 +++++- 8 files changed, 34 insertions(+), 12 deletions(-) diff --git a/.github/workflows/trigger-tasks-deploy-main.yml b/.github/workflows/trigger-tasks-deploy-main.yml index a5ca8edf04..afa037a84a 100644 --- a/.github/workflows/trigger-tasks-deploy-main.yml +++ b/.github/workflows/trigger-tasks-deploy-main.yml @@ -36,9 +36,9 @@ jobs: - name: Copy schema to app and generate client working-directory: ./apps/app run: | - mkdir -p prisma - cp ../../packages/db/dist/schema.prisma prisma/schema.prisma - bunx prisma generate + mkdir -p prisma/schema + find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \; + bunx prisma generate --schema=prisma/schema - name: 🚀 Deploy Trigger.dev working-directory: ./apps/app timeout-minutes: 20 diff --git a/.github/workflows/trigger-tasks-deploy-release.yml b/.github/workflows/trigger-tasks-deploy-release.yml index b69ecd5eb2..eb578b548c 100644 --- a/.github/workflows/trigger-tasks-deploy-release.yml +++ b/.github/workflows/trigger-tasks-deploy-release.yml @@ -42,9 +42,9 @@ jobs: - name: Copy schema to app and generate client working-directory: ./apps/app run: | - mkdir -p prisma - cp ../../packages/db/dist/schema.prisma prisma/schema.prisma - bunx prisma generate + mkdir -p prisma/schema + find ../../packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} prisma/schema/ \; + bunx prisma generate --schema=prisma/schema - name: 🚀 Deploy Trigger.dev working-directory: ./apps/app diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index efa583aeb7..d6cec384cc 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -6,7 +6,11 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); + // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), + // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). + const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; + const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index 4630094945..8018097814 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -6,7 +6,11 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); + // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), + // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). + const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; + const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index efa583aeb7..d6cec384cc 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -6,7 +6,11 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); + // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), + // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). + const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; + const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index 4630094945..8018097814 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -6,7 +6,11 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); + // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), + // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). + const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; + const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 2c68ebde67..1de46f4e4e 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -57,7 +57,9 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(url); - const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); + const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; + const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index efa583aeb7..d6cec384cc 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -6,7 +6,11 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient }; function createPrismaClient(): PrismaClient { const url = process.env.DATABASE_URL!; const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); - const adapter = new PrismaPg({ connectionString: url, ssl: isLocalhost ? undefined : true }); + // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), + // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). + const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; + const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } From 335dcd280afac73bfc895ab8a74af110e85faff6 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 2 Apr 2026 13:31:24 -0400 Subject: [PATCH 29/31] fix: strip sslmode from DATABASE_URL to avoid conflict with explicit ssl option (#2435) PrismaPg receives both `sslmode=require` in the connection string and an explicit `ssl` option. This double-SSL configuration can cause intermittent connection failures on staging (ECS + RDS). Uses the URL API to safely remove the sslmode param instead of the old buggy regex approach. Co-authored-by: Claude Opus 4.6 (1M context) --- apps/api/prisma/client.ts | 12 +++- apps/app/prisma/client.ts | 12 +++- apps/framework-editor/prisma/client.ts | 12 +++- apps/portal/prisma/client.ts | 12 +++- packages/db/scripts/combine-schemas.js | 11 +++- packages/db/src/client.ts | 12 +++- packages/db/src/strip-ssl-mode.test.ts | 85 ++++++++++++++++++++++++++ 7 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 packages/db/src/strip-ssl-mode.test.ts diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts index d6cec384cc..573debfd25 100644 --- a/apps/api/prisma/client.ts +++ b/apps/api/prisma/client.ts @@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL!; - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const rawUrl = process.env.DATABASE_URL!; + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl); // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + // Strip sslmode from the connection string to avoid conflicts with the explicit ssl option + const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl; const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts index 8018097814..d48e37720f 100644 --- a/apps/app/prisma/client.ts +++ b/apps/app/prisma/client.ts @@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL!; - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const rawUrl = process.env.DATABASE_URL!; + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl); // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + // Strip sslmode from the connection string to avoid conflicts with the explicit ssl option + const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl; const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/framework-editor/prisma/client.ts b/apps/framework-editor/prisma/client.ts index d6cec384cc..573debfd25 100644 --- a/apps/framework-editor/prisma/client.ts +++ b/apps/framework-editor/prisma/client.ts @@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL!; - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const rawUrl = process.env.DATABASE_URL!; + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl); // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + // Strip sslmode from the connection string to avoid conflicts with the explicit ssl option + const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl; const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/apps/portal/prisma/client.ts b/apps/portal/prisma/client.ts index 8018097814..d48e37720f 100644 --- a/apps/portal/prisma/client.ts +++ b/apps/portal/prisma/client.ts @@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL!; - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const rawUrl = process.env.DATABASE_URL!; + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl); // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + // Strip sslmode from the connection string to avoid conflicts with the explicit ssl option + const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl; const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/scripts/combine-schemas.js b/packages/db/scripts/combine-schemas.js index 1de46f4e4e..1b7212a41c 100755 --- a/packages/db/scripts/combine-schemas.js +++ b/packages/db/scripts/combine-schemas.js @@ -54,11 +54,18 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL!; - const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(url); + const rawUrl = process.env.DATABASE_URL!; + const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(rawUrl); const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl; const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index d6cec384cc..573debfd25 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL!; - const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url); + const rawUrl = process.env.DATABASE_URL!; + const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl); // Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle), // otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments). const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS; const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false }; + // Strip sslmode from the connection string to avoid conflicts with the explicit ssl option + const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl; const adapter = new PrismaPg({ connectionString: url, ssl }); return new PrismaClient({ adapter }); } diff --git a/packages/db/src/strip-ssl-mode.test.ts b/packages/db/src/strip-ssl-mode.test.ts new file mode 100644 index 0000000000..1aa9bd5c3a --- /dev/null +++ b/packages/db/src/strip-ssl-mode.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'bun:test'; + +function stripSslMode(connectionString: string): string { + const url = new URL(connectionString); + url.searchParams.delete('sslmode'); + return url.toString(); +} + +describe('stripSslMode', () => { + it('removes sslmode=require from the connection string', () => { + const input = + 'postgresql://user:pass@host.rds.amazonaws.com:5432/mydb?sslmode=require'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:pass@host.rds.amazonaws.com:5432/mydb', + ); + }); + + it('removes sslmode when it is one of multiple params', () => { + const input = + 'postgresql://user:pass@host:5432/mydb?sslmode=require&connection_limit=50'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:pass@host:5432/mydb?connection_limit=50', + ); + }); + + it('preserves other query params when sslmode is first', () => { + const input = + 'postgresql://user:pass@host:5432/mydb?sslmode=require&pgbouncer=true&connection_limit=10'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:pass@host:5432/mydb?pgbouncer=true&connection_limit=10', + ); + }); + + it('preserves other query params when sslmode is in the middle', () => { + const input = + 'postgresql://user:pass@host:5432/mydb?pgbouncer=true&sslmode=require&connection_limit=10'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:pass@host:5432/mydb?pgbouncer=true&connection_limit=10', + ); + }); + + it('preserves other query params when sslmode is last', () => { + const input = + 'postgresql://user:pass@host:5432/mydb?connection_limit=10&sslmode=require'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:pass@host:5432/mydb?connection_limit=10', + ); + }); + + it('handles different sslmode values', () => { + const input = + 'postgresql://user:pass@host:5432/mydb?sslmode=verify-full'; + const result = stripSslMode(input); + expect(result).toBe('postgresql://user:pass@host:5432/mydb'); + }); + + it('returns url unchanged when no sslmode is present', () => { + const input = + 'postgresql://user:pass@host:5432/mydb?connection_limit=50'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:pass@host:5432/mydb?connection_limit=50', + ); + }); + + it('handles url with no query params', () => { + const input = 'postgresql://user:pass@host:5432/mydb'; + const result = stripSslMode(input); + expect(result).toBe('postgresql://user:pass@host:5432/mydb'); + }); + + it('preserves password with special characters', () => { + const input = + 'postgresql://user:p%40ss%23word@host:5432/mydb?sslmode=require&connection_limit=50'; + const result = stripSslMode(input); + expect(result).toBe( + 'postgresql://user:p%40ss%23word@host:5432/mydb?connection_limit=50', + ); + }); +}); From 64e7e0ac69e9921cb43fb84f1f879025d027a79b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:35:41 -0400 Subject: [PATCH 30/31] [dev] [Marfuen] mariano/fix-strip-sslmode-from-connection-string (#2436) * fix: strip sslmode from DATABASE_URL to avoid conflict with explicit ssl option PrismaPg receives both `sslmode=require` in the connection string and an explicit `ssl` option. This double-SSL configuration can cause intermittent connection failures on staging (ECS + RDS). Uses the URL API to safely remove the sslmode param instead of the old buggy regex approach. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: exclude test files from tsc compilation in packages/db The bun:test import in strip-ssl-mode.test.ts breaks the Docker build which uses tsc (not bun) to compile packages/db. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Mariano Fuentes Co-authored-by: Claude Opus 4.6 (1M context) --- packages/db/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 041ff12a62..d979a7a0d5 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -19,5 +19,5 @@ "declarationMap": true }, "include": ["src"], - "exclude": ["node_modules", "dist", "src/generated"] + "exclude": ["node_modules", "dist", "src/generated", "src/**/*.test.ts"] } From cf9990b5dda3a857710460ca8ad69639d37bc6d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:48:46 -0400 Subject: [PATCH 31/31] [dev] [Marfuen] mariano/fix-strip-sslmode-from-connection-string (#2437) * fix: strip sslmode from DATABASE_URL to avoid conflict with explicit ssl option PrismaPg receives both `sslmode=require` in the connection string and an explicit `ssl` option. This double-SSL configuration can cause intermittent connection failures on staging (ECS + RDS). Uses the URL API to safely remove the sslmode param instead of the old buggy regex approach. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: exclude test files from tsc compilation in packages/db The bun:test import in strip-ssl-mode.test.ts breaks the Docker build which uses tsc (not bun) to compile packages/db. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove redundant prisma generate from Docker build The @prisma/client is already generated by packages/db build step (generate-prisma-client-js.js). The second prisma generate in the API build step was redundant and failing with an empty error in Docker. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: update Trigger.dev extensions to copy entire schema directory Both customPrismaExtension.ts files (api + app) now copy the full multi-file schema directory instead of a single file. This ensures prisma generate sees all model files, not just the generator/datasource. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Mariano Fuentes Co-authored-by: Claude Opus 4.6 (1M context) --- apps/api/Dockerfile.multistage | 5 +++-- apps/api/customPrismaExtension.ts | 36 +++++++++++++++++------------- apps/app/customPrismaExtension.ts | 37 ++++++++++++++++++------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/apps/api/Dockerfile.multistage b/apps/api/Dockerfile.multistage index 045cf48834..90e1c5f38d 100644 --- a/apps/api/Dockerfile.multistage +++ b/apps/api/Dockerfile.multistage @@ -68,9 +68,10 @@ RUN cd packages/auth && bun run build \ && cd ../email && bun run build \ && cd ../company && bun run build -# Copy model files to api schema dir and generate Prisma client, then build NestJS app +# Copy model files to api schema dir, then build NestJS app +# Note: @prisma/client is already generated by packages/db build (generate-prisma-client-js.js) RUN find /app/packages/db/prisma/schema -name '*.prisma' ! -name 'schema.prisma' -exec cp {} /app/apps/api/prisma/schema/ \; \ - && cd /app/apps/api && /app/node_modules/.bin/prisma generate --schema=prisma/schema && bunx nest build + && cd /app/apps/api && bunx nest build # ============================================================================= # STAGE 3: Production Runtime diff --git a/apps/api/customPrismaExtension.ts b/apps/api/customPrismaExtension.ts index bca8da0220..6848fd7645 100644 --- a/apps/api/customPrismaExtension.ts +++ b/apps/api/customPrismaExtension.ts @@ -114,22 +114,23 @@ export class PrismaExtension implements BuildExtension { const env: Record = {}; // Copy the prisma schema from the published package to the build output path - const schemaDestinationPath = join(manifest.outputPath, 'prisma', 'schema.prisma'); - const schemaDestinationDir = dirname(schemaDestinationPath); + // Copy the entire schema directory (multi-file schema) + const sourceDir = dirname(schemaPath); + const schemaDestinationDir = join(manifest.outputPath, 'prisma', 'schema'); context.logger.debug( - `Copying the prisma schema from ${schemaPath} to ${schemaDestinationPath}`, + `Copying the prisma schema directory from ${sourceDir} to ${schemaDestinationDir}`, ); await mkdir(schemaDestinationDir, { recursive: true }); - await cp(schemaPath, schemaDestinationPath); + await cp(sourceDir, schemaDestinationDir, { recursive: true }); - // Patch the schema to use prisma-client-js (populates @prisma/client at runtime) + // Patch schema.prisma to use prisma-client-js (populates @prisma/client at runtime) commands.push( - `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema.prisma`, + `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema/schema.prisma`, ); - // Add prisma generate command to generate the client from the patched schema + // Generate client from the multi-file schema directory commands.push( - `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma`, + `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema`, ); // Only handle migrations if requested @@ -190,19 +191,22 @@ export class PrismaExtension implements BuildExtension { context: ExtendedBuildContext, schemaSourcePath: string, ): Promise { - const schemaDir = resolve(context.workingDir, 'prisma'); - const schemaDestinationPath = resolve(schemaDir, 'schema.prisma'); + // schemaSourcePath points to a file inside the schema directory. + // Copy the entire directory (multi-file schema) to the local prisma/schema/ dir. + const sourceDir = dirname(schemaSourcePath); + const localSchemaDir = resolve(context.workingDir, 'prisma', 'schema'); - await mkdir(schemaDir, { recursive: true }); - await cp(schemaSourcePath, schemaDestinationPath); + await mkdir(localSchemaDir, { recursive: true }); + await cp(sourceDir, localSchemaDir, { recursive: true }); - // Patch schema to use prisma-client-js (default output → @prisma/client) + // Patch schema.prisma to use prisma-client-js (default output → @prisma/client) + const localSchemaFile = resolve(localSchemaDir, 'schema.prisma'); const { readFileSync, writeFileSync } = await import('node:fs'); - let schemaContent = readFileSync(schemaDestinationPath, 'utf8'); + let schemaContent = readFileSync(localSchemaFile, 'utf8'); schemaContent = schemaContent .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); - writeFileSync(schemaDestinationPath, schemaContent); + writeFileSync(localSchemaFile, schemaContent); const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js'); @@ -221,7 +225,7 @@ export class PrismaExtension implements BuildExtension { } context.logger.log('Prisma client missing. Generating before Trigger indexing.'); - await this.runPrismaGenerate(context, prismaBinary, schemaDestinationPath); + await this.runPrismaGenerate(context, prismaBinary, localSchemaDir); } private runPrismaGenerate( diff --git a/apps/app/customPrismaExtension.ts b/apps/app/customPrismaExtension.ts index d77a5d88a2..88d06a16b7 100644 --- a/apps/app/customPrismaExtension.ts +++ b/apps/app/customPrismaExtension.ts @@ -114,21 +114,23 @@ export class PrismaExtension implements BuildExtension { const env: Record = {}; // Copy the prisma schema from the published package to the build output path - const schemaDestinationPath = join(manifest.outputPath, 'prisma', 'schema.prisma'); + // Copy the entire schema directory (multi-file schema) + const sourceDir = dirname(schemaPath); + const schemaDestinationDir = join(manifest.outputPath, 'prisma', 'schema'); context.logger.debug( - `Copying the prisma schema from ${schemaPath} to ${schemaDestinationPath}`, + `Copying the prisma schema directory from ${sourceDir} to ${schemaDestinationDir}`, ); - await cp(schemaPath, schemaDestinationPath); + await mkdir(schemaDestinationDir, { recursive: true }); + await cp(sourceDir, schemaDestinationDir, { recursive: true }); - // Patch the schema to use prisma-client-js (CJS-compatible, populates @prisma/client) - // The published schema uses prisma-client provider which generates .ts files — not suitable for Node.js runtime + // Patch schema.prisma to use prisma-client-js (populates @prisma/client at runtime) commands.push( - `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema.prisma`, + `sed -i 's/provider.*=.*"prisma-client"/provider = "prisma-client-js"/' ./prisma/schema/schema.prisma && sed -i '/output.*=.*"/d' ./prisma/schema/schema.prisma`, ); - // Add prisma generate command to generate the client from the patched schema + // Generate client from the multi-file schema directory commands.push( - `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema.prisma`, + `${binaryForRuntime(manifest.runtime)} node_modules/prisma/build/index.js generate --schema=./prisma/schema`, ); // Only handle migrations if requested @@ -189,19 +191,22 @@ export class PrismaExtension implements BuildExtension { context: ExtendedBuildContext, schemaSourcePath: string, ): Promise { - const schemaDir = resolve(context.workingDir, 'prisma'); - const schemaDestinationPath = resolve(schemaDir, 'schema.prisma'); + // schemaSourcePath points to a file inside the schema directory. + // Copy the entire directory (multi-file schema) to the local prisma/schema/ dir. + const sourceDir = dirname(schemaSourcePath); + const localSchemaDir = resolve(context.workingDir, 'prisma', 'schema'); - await mkdir(schemaDir, { recursive: true }); - await cp(schemaSourcePath, schemaDestinationPath); + await mkdir(localSchemaDir, { recursive: true }); + await cp(sourceDir, localSchemaDir, { recursive: true }); - // Patch schema to use prisma-client-js (default output → @prisma/client) + // Patch schema.prisma to use prisma-client-js (default output → @prisma/client) + const localSchemaFile = resolve(localSchemaDir, 'schema.prisma'); const { readFileSync, writeFileSync } = await import('node:fs'); - let schemaContent = readFileSync(schemaDestinationPath, 'utf8'); + let schemaContent = readFileSync(localSchemaFile, 'utf8'); schemaContent = schemaContent .replace(/provider\s*=\s*"prisma-client"/g, 'provider = "prisma-client-js"') .replace(/\s*output\s*=\s*"[^"]*"\n?/g, '\n'); - writeFileSync(schemaDestinationPath, schemaContent); + writeFileSync(localSchemaFile, schemaContent); const clientEntryPoint = resolve(context.workingDir, 'node_modules/.prisma/client/default.js'); @@ -220,7 +225,7 @@ export class PrismaExtension implements BuildExtension { } context.logger.log('Prisma client missing. Generating before Trigger indexing.'); - await this.runPrismaGenerate(context, prismaBinary, schemaDestinationPath); + await this.runPrismaGenerate(context, prismaBinary, localSchemaDir); } private runPrismaGenerate(