diff --git a/apps/app/src/app/(app)/[orgId]/settings/layout.tsx b/apps/app/src/app/(app)/[orgId]/settings/layout.tsx
index 4f6f5e7d0..734aa078a 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/layout.tsx
+++ b/apps/app/src/app/(app)/[orgId]/settings/layout.tsx
@@ -24,10 +24,6 @@ export default async function Layout({ children }: { children: React.ReactNode }
path: `/${orgId}/settings`,
label: 'General',
},
- {
- path: `/${orgId}/settings/trust-portal`,
- label: 'Trust Portal',
- },
{
path: `/${orgId}/settings/context-hub`,
label: 'Context',
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/loading.tsx b/apps/app/src/app/(app)/[orgId]/settings/trust-portal/loading.tsx
deleted file mode 100644
index 4f38f9a92..000000000
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/loading.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import Loader from '@/components/ui/loader';
-
-export default function Loading() {
- return (
-
-
-
- );
-}
diff --git a/apps/app/src/app/(app)/[orgId]/trust/layout.tsx b/apps/app/src/app/(app)/[orgId]/trust/layout.tsx
new file mode 100644
index 000000000..fdcdf03c1
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/trust/layout.tsx
@@ -0,0 +1,30 @@
+import { SecondaryMenu } from '@comp/ui/secondary-menu';
+
+export default async function Layout({
+ children,
+ params
+}: {
+ children: React.ReactNode;
+ params: Promise<{ orgId: string }>;
+}) {
+ const { orgId } = await params;
+
+ return (
+
+ );
+}
+
diff --git a/apps/app/src/app/(app)/[orgId]/trust/page.tsx b/apps/app/src/app/(app)/[orgId]/trust/page.tsx
index 967e3e262..20f4cc244 100644
--- a/apps/app/src/app/(app)/[orgId]/trust/page.tsx
+++ b/apps/app/src/app/(app)/[orgId]/trust/page.tsx
@@ -6,22 +6,20 @@ export default async function TrustAccessPage({ params }: { params: Promise<{ or
const { orgId } = await params;
return (
-
-
-
-
-
Trust Access Management
-
Manage data access requests and grants
-
-
+
+
+
+
Access & Grants
+
Manage data access requests and grants
-
-
+
+
+
);
}
export async function generateMetadata(): Promise
{
return {
- title: 'Trust Access Management',
+ title: 'Access & Grants',
};
}
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/check-dns-record.ts b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts
similarity index 97%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/check-dns-record.ts
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts
index 0c2cc28b1..a07b9310e 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/check-dns-record.ts
+++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts
@@ -162,7 +162,8 @@ export const checkDnsRecordAction = authActionClient
},
});
- revalidatePath(`/${activeOrgId}/settings/trust-portal`);
+ revalidatePath(`/${activeOrgId}/trust`);
+ revalidatePath(`/${activeOrgId}/trust/portal-settings`);
revalidateTag(`organization_${activeOrgId}`);
return {
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/custom-domain.ts b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/custom-domain.ts
similarity index 96%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/custom-domain.ts
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/custom-domain.ts
index c2039536e..8ed7589bb 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/custom-domain.ts
+++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/custom-domain.ts
@@ -100,7 +100,8 @@ export const customDomainAction = authActionClient
},
});
- revalidatePath(`/${activeOrganizationId}/settings/trust-portal`);
+ revalidatePath(`/${activeOrganizationId}/trust`);
+ revalidatePath(`/${activeOrganizationId}/trust/portal-settings`);
revalidateTag(`organization_${activeOrganizationId}`);
return {
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/is-friendly-available.ts b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/is-friendly-available.ts
similarity index 100%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/is-friendly-available.ts
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/is-friendly-available.ts
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/trust-portal-switch.ts b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/trust-portal-switch.ts
similarity index 93%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/trust-portal-switch.ts
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/trust-portal-switch.ts
index 5c8522e2f..384e8ff57 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/trust-portal-switch.ts
+++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/trust-portal-switch.ts
@@ -48,7 +48,8 @@ export const trustPortalSwitchAction = authActionClient
},
});
- revalidatePath(`/${activeOrganizationId}/settings/trust-portal`);
+ revalidatePath(`/${activeOrganizationId}/trust`);
+ revalidatePath(`/${activeOrganizationId}/trust/portal-settings`);
revalidateTag(`organization_${activeOrganizationId}`);
return {
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/update-trust-portal-frameworks.ts b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/update-trust-portal-frameworks.ts
similarity index 97%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/update-trust-portal-frameworks.ts
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/update-trust-portal-frameworks.ts
index dd705077d..bd5b8957e 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/actions/update-trust-portal-frameworks.ts
+++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/update-trust-portal-frameworks.ts
@@ -94,6 +94,7 @@ export async function updateTrustPortalFrameworks({
},
});
- revalidatePath(`/${orgId}/settings/trust-portal`);
+ revalidatePath(`/${orgId}/trust`);
+ revalidatePath(`/${orgId}/trust/portal-settings`);
revalidateTag(`organization_${orgId}`);
}
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/TrustPortalDomain.tsx b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/TrustPortalDomain.tsx
similarity index 100%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/TrustPortalDomain.tsx
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/TrustPortalDomain.tsx
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/TrustPortalSwitch.tsx b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/TrustPortalSwitch.tsx
similarity index 91%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/TrustPortalSwitch.tsx
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/TrustPortalSwitch.tsx
index 60918884e..5f9db7e2b 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/TrustPortalSwitch.tsx
+++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/TrustPortalSwitch.tsx
@@ -32,6 +32,7 @@ import {
SOC2Type2,
} from './logos';
+// Client-side form schema (includes all fields for form state)
const trustPortalSwitchSchema = z.object({
enabled: z.boolean(),
contactEmail: z.string().email().or(z.literal('')).optional(),
@@ -56,6 +57,13 @@ const trustPortalSwitchSchema = z.object({
iso9001Status: z.enum(['started', 'in_progress', 'compliant']),
});
+// Server action input schema (only fields that the server accepts)
+type TrustPortalSwitchActionInput = {
+ enabled: boolean;
+ contactEmail?: string | '';
+ friendlyUrl?: string;
+};
+
const FRAMEWORK_KEY_TO_API_SLUG: Record = {
iso27001: 'iso_27001',
iso42001: 'iso_42001',
@@ -272,6 +280,10 @@ export function TrustPortalSwitch({
},
});
+ // Use ref to store latest trustPortalSwitch to avoid stale closures
+ const trustPortalSwitchRef = useRef(trustPortalSwitch);
+ trustPortalSwitchRef.current = trustPortalSwitch;
+
const checkFriendlyUrl = useAction(isFriendlyAvailable);
const form = useForm>({
@@ -302,10 +314,10 @@ export function TrustPortalSwitch({
});
const onSubmit = useCallback(
- async (data: z.infer) => {
- await trustPortalSwitch.execute(data);
+ async (data: TrustPortalSwitchActionInput) => {
+ await trustPortalSwitchRef.current.execute(data);
},
- [], // Remove trustPortalSwitch from dependencies to prevent infinite loop
+ [], // Safe to use empty array because we use ref
);
const portalUrl = domainVerified ? `https://${domain}` : `https://trust.inc/${slug}`;
@@ -316,28 +328,51 @@ export function TrustPortalSwitch({
enabled: enabled,
});
+ const savingRef = useRef<{ [key: string]: boolean }>({
+ contactEmail: false,
+ friendlyUrl: false,
+ enabled: false,
+ });
+
const autoSave = useCallback(
async (field: string, value: any) => {
+ // Prevent concurrent saves for the same field
+ if (savingRef.current[field]) {
+ return;
+ }
+
const current = form.getValues();
if (lastSaved.current[field] !== value) {
- const data = { ...current, [field]: value };
- await onSubmit(data);
- lastSaved.current[field] = value;
+ savingRef.current[field] = true;
+ try {
+ // Only send fields that trustPortalSwitchAction accepts
+ // Server schema only accepts: enabled, contactEmail, friendlyUrl
+ const data: TrustPortalSwitchActionInput = {
+ enabled: field === 'enabled' ? value : current.enabled,
+ contactEmail: field === 'contactEmail' ? value : current.contactEmail ?? '',
+ friendlyUrl: field === 'friendlyUrl' ? value : current.friendlyUrl ?? undefined,
+ };
+ await onSubmit(data);
+ lastSaved.current[field] = value;
+ } finally {
+ savingRef.current[field] = false;
+ }
}
},
[form, onSubmit],
);
const [contactEmailValue, setContactEmailValue] = useState(form.getValues('contactEmail') || '');
- const debouncedContactEmail = useDebounce(contactEmailValue, 500);
+ const debouncedContactEmail = useDebounce(contactEmailValue, 800);
useEffect(() => {
if (
debouncedContactEmail !== undefined &&
- debouncedContactEmail !== lastSaved.current.contactEmail
+ debouncedContactEmail !== lastSaved.current.contactEmail &&
+ !savingRef.current.contactEmail
) {
form.setValue('contactEmail', debouncedContactEmail);
- autoSave('contactEmail', debouncedContactEmail);
+ void autoSave('contactEmail', debouncedContactEmail);
}
}, [debouncedContactEmail, autoSave, form]);
@@ -351,30 +386,62 @@ export function TrustPortalSwitch({
);
const [friendlyUrlValue, setFriendlyUrlValue] = useState(form.getValues('friendlyUrl') || '');
- const debouncedFriendlyUrl = useDebounce(friendlyUrlValue, 500);
+ const debouncedFriendlyUrl = useDebounce(friendlyUrlValue, 700);
const [friendlyUrlStatus, setFriendlyUrlStatus] = useState<
'idle' | 'checking' | 'available' | 'unavailable'
>('idle');
+ const lastCheckedUrlRef = useRef('');
+ const processingResultRef = useRef('');
useEffect(() => {
if (!debouncedFriendlyUrl || debouncedFriendlyUrl === (friendlyUrl ?? '')) {
setFriendlyUrlStatus('idle');
+ lastCheckedUrlRef.current = '';
+ processingResultRef.current = '';
+ return;
+ }
+
+ // Only check if we haven't already checked this exact value
+ if (lastCheckedUrlRef.current === debouncedFriendlyUrl) {
return;
}
+
+ lastCheckedUrlRef.current = debouncedFriendlyUrl;
+ processingResultRef.current = '';
setFriendlyUrlStatus('checking');
checkFriendlyUrl.execute({ friendlyUrl: debouncedFriendlyUrl, orgId });
}, [debouncedFriendlyUrl, orgId, friendlyUrl]);
+
useEffect(() => {
if (checkFriendlyUrl.status === 'executing') return;
- if (checkFriendlyUrl.result?.data?.isAvailable === true) {
+
+ const result = checkFriendlyUrl.result?.data;
+ const checkedUrl = lastCheckedUrlRef.current;
+
+ // Only process if this result matches the currently checked URL
+ if (checkedUrl !== debouncedFriendlyUrl || !checkedUrl) {
+ return;
+ }
+
+ // Prevent processing the same result multiple times
+ if (processingResultRef.current === checkedUrl) {
+ return;
+ }
+
+ if (result?.isAvailable === true) {
setFriendlyUrlStatus('available');
+ processingResultRef.current = checkedUrl;
- if (debouncedFriendlyUrl !== lastSaved.current.friendlyUrl) {
+ if (
+ debouncedFriendlyUrl !== lastSaved.current.friendlyUrl &&
+ !savingRef.current.friendlyUrl
+ ) {
form.setValue('friendlyUrl', debouncedFriendlyUrl);
- autoSave('friendlyUrl', debouncedFriendlyUrl);
+ void autoSave('friendlyUrl', debouncedFriendlyUrl);
}
- } else if (checkFriendlyUrl.result?.data?.isAvailable === false) {
+ } else if (result?.isAvailable === false) {
setFriendlyUrlStatus('unavailable');
+ processingResultRef.current = checkedUrl;
}
}, [checkFriendlyUrl.status, checkFriendlyUrl.result, debouncedFriendlyUrl, form, autoSave]);
@@ -400,42 +467,40 @@ export function TrustPortalSwitch({
return (
);
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/logos.tsx b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/logos.tsx
similarity index 100%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/logos.tsx
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/logos.tsx
diff --git a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/page.tsx b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/page.tsx
similarity index 62%
rename from apps/app/src/app/(app)/[orgId]/settings/trust-portal/page.tsx
rename to apps/app/src/app/(app)/[orgId]/trust/portal-settings/page.tsx
index ade645860..9d97be0a8 100644
--- a/apps/app/src/app/(app)/[orgId]/settings/trust-portal/page.tsx
+++ b/apps/app/src/app/(app)/[orgId]/trust/portal-settings/page.tsx
@@ -1,71 +1,75 @@
+import PageCore from '@/components/pages/PageCore.tsx';
+import type { Metadata } from 'next';
import { auth } from '@/utils/auth';
import { env } from '@/env.mjs';
import { db } from '@db';
-import type { Metadata } from 'next';
import { headers } from 'next/headers';
-import { cache } from 'react';
-import { TrustPortalDomain } from './components/TrustPortalDomain';
import { TrustPortalSwitch } from './components/TrustPortalSwitch';
+import { TrustPortalDomain } from './components/TrustPortalDomain';
-export default async function TrustPortalSettings({
- params,
-}: {
- params: Promise<{ orgId: string }>;
-}) {
+export default async function PortalSettingsPage({ params }: { params: Promise<{ orgId: string }> }) {
const { orgId } = await params;
const trustPortal = await getTrustPortal(orgId);
const certificateFiles = await fetchComplianceCertificates(orgId);
return (
-
-
-
-
+
+
+
+
Portal Settings
+
Configure your trust portal
+
+
+
+
+
+
+
);
}
-const getTrustPortal = cache(async (orgId: string) => {
+const getTrustPortal = async (orgId: string) => {
const session = await auth.api.getSession({
headers: await headers(),
});
@@ -110,7 +114,7 @@ const getTrustPortal = cache(async (orgId: string) => {
vercelVerification: trustPortal?.vercelVerification,
friendlyUrl: trustPortal?.friendlyUrl,
};
-});
+};
type CertificateFiles = {
iso27001FileName: string | null;
@@ -165,7 +169,6 @@ async function fetchComplianceCertificates(orgId: string): Promise {
}
}
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ locale: string }>;
-}): Promise {
+export async function generateMetadata(): Promise {
return {
- title: 'Trust Portal',
+ title: 'Portal Settings',
};
}
+
diff --git a/apps/portal/package.json b/apps/portal/package.json
index ad6d53685..718deb34b 100644
--- a/apps/portal/package.json
+++ b/apps/portal/package.json
@@ -16,7 +16,7 @@
"@types/jszip": "^3.4.1",
"@upstash/ratelimit": "^2.0.5",
"archiver": "^7.0.1",
- "better-auth": "^1.3.27",
+ "better-auth": "^1.4.5",
"class-variance-authority": "^0.7.1",
"geist": "^1.3.1",
"jszip": "^3.10.1",
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
index 06e715624..a9dfdde40 100644
--- a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
+++ b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
@@ -10,63 +10,57 @@ import { OrganizationDashboard } from './components/OrganizationDashboard';
import type { FleetPolicy, Host } from './types';
export default async function OrganizationPage({ params }: { params: Promise<{ orgId: string }> }) {
- try {
- const { orgId } = await params;
-
- const session = await auth.api.getSession({
- headers: await headers(),
- });
-
- if (!session?.user) {
- return redirect('/auth');
- }
-
- let member = null;
-
- try {
- member = await db.member.findFirst({
- where: {
- userId: session.user.id,
- organizationId: orgId,
- deactivated: false,
- },
- include: {
- user: true,
- organization: true, // Include organization details
- },
- });
- } catch (error) {
- console.error('Error fetching member:', error);
- // Return a fallback UI or redirect to error page
- return redirect('/');
- }
-
- if (!member) {
- return redirect('/'); // Or appropriate login/auth route
- }
-
- // Only fetch fleet policies if fleet is enabled
- let fleetPolicies: FleetPolicy[] = [];
- let device: Host | null = null;
+ const { orgId } = await params;
+
+ // Auth check with error handling
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ }).catch((error) => {
+ console.error('Error getting session:', error);
+ redirect('/');
+ });
+
+ if (!session?.user) {
+ redirect('/auth');
+ }
- const fleetData = await getFleetPolicies(member);
- fleetPolicies = fleetData.fleetPolicies;
- device = fleetData.device;
+ // Fetch member with error handling
+ let member;
- return (
-
- );
+ try {
+ member = await db.member.findFirst({
+ where: {
+ userId: session.user.id,
+ organizationId: orgId,
+ deactivated: false,
+ },
+ include: {
+ user: true,
+ organization: true,
+ },
+ });
} catch (error) {
- console.error('Error in OrganizationPage:', { error });
- // Redirect to a safe page if there's an unexpected error
- return redirect('/');
+ console.error('Error fetching member:', error);
+ redirect('/');
+ }
+
+ // Member check - redirect happens outside try-catch
+ if (!member) {
+ redirect('/');
}
+
+ // Fleet policies - already has graceful error handling in getFleetPolicies
+ const fleetData = await getFleetPolicies(member);
+
+ return (
+
+ );
}
const getFleetPolicies = async (
diff --git a/apps/portal/src/app/actions/login.ts b/apps/portal/src/app/actions/login.ts
index 2a26187d2..919497390 100644
--- a/apps/portal/src/app/actions/login.ts
+++ b/apps/portal/src/app/actions/login.ts
@@ -2,6 +2,7 @@
import { auth } from '@/app/lib/auth';
import { createSafeActionClient } from 'next-safe-action';
+import { headers } from 'next/headers';
import { z } from 'zod';
const handleServerError = (e: Error) => {
@@ -28,7 +29,7 @@ const handleServerError = (e: Error) => {
}
if (errorMessage.includes('too many attempts')) {
- return 'Too many requests. Please try again later.';
+ return 'Too many requests. Please try again later.';
}
// If we can't match a specific error, throw a generic but helpful message
@@ -46,11 +47,15 @@ export const login = createSafeActionClient({ handleServerError })
}),
)
.action(async ({ parsedInput }) => {
+ const headersList = await headers();
+
await auth.api.signInEmailOTP({
+ headers: headersList,
body: {
email: parsedInput.email,
otp: parsedInput.otp,
},
+ asResponse: true,
});
return {
diff --git a/apps/portal/src/app/lib/auth.ts b/apps/portal/src/app/lib/auth.ts
index 277affdc1..05ada8559 100644
--- a/apps/portal/src/app/lib/auth.ts
+++ b/apps/portal/src/app/lib/auth.ts
@@ -12,9 +12,11 @@ export const auth = betterAuth({
provider: 'postgresql',
}),
advanced: {
- // This will enable us to fall back to DB for ID generation.
- // It's important so we can use custom IDs specified in Prisma Schema.
- generateId: false,
+ database: {
+ // This will enable us to fall back to DB for ID generation.
+ // It's important so we can use custom IDs specified in Prisma Schema.
+ generateId: false,
+ },
},
trustedOrigins: ['http://localhost:3000', 'https://*.trycomp.ai'],
secret: env.AUTH_SECRET!,
diff --git a/bun.lock b/bun.lock
index f83df744a..40c1c4c07 100644
--- a/bun.lock
+++ b/bun.lock
@@ -8,6 +8,7 @@
"@types/cheerio": "^1.0.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@upstash/vector": "^1.2.2",
+ "better-auth": "1.4.5",
"cheerio": "^1.1.2",
"react-syntax-highlighter": "^15.6.6",
"unpdf": "^1.4.0",
diff --git a/package.json b/package.json
index 22548f1f2..0255191c5 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"@types/cheerio": "^1.0.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@upstash/vector": "^1.2.2",
+ "better-auth": "1.4.5",
"cheerio": "^1.1.2",
"react-syntax-highlighter": "^15.6.6",
"unpdf": "^1.4.0",
diff --git a/packages/docs/docs.json b/packages/docs/docs.json
index 1116e3909..16c6a7c4e 100644
--- a/packages/docs/docs.json
+++ b/packages/docs/docs.json
@@ -21,7 +21,13 @@
"automated-evidence",
"device-agent",
"security-questionnaire",
- "trust-access"
+ {
+ "group": "Trust",
+ "pages": [
+ "trust-access",
+ "security-questionnaire-trust-center"
+ ]
+ }
]
}
]
diff --git a/packages/docs/images/compliance-resources-section.png b/packages/docs/images/compliance-resources-section.png
new file mode 100644
index 000000000..2e80f1b17
Binary files /dev/null and b/packages/docs/images/compliance-resources-section.png differ
diff --git a/packages/docs/images/security-questionnaire-section.png b/packages/docs/images/security-questionnaire-section.png
new file mode 100644
index 000000000..42f793251
Binary files /dev/null and b/packages/docs/images/security-questionnaire-section.png differ
diff --git a/packages/docs/images/trust-nav-tabs.png b/packages/docs/images/trust-nav-tabs.png
new file mode 100644
index 000000000..f207f1d22
Binary files /dev/null and b/packages/docs/images/trust-nav-tabs.png differ
diff --git a/packages/docs/security-questionnaire-trust-center.mdx b/packages/docs/security-questionnaire-trust-center.mdx
new file mode 100644
index 000000000..c400ce8b3
--- /dev/null
+++ b/packages/docs/security-questionnaire-trust-center.mdx
@@ -0,0 +1,511 @@
+---
+title: 'Security Questionnaire'
+description: 'A comprehensive guide to using the Security Questionnaire API for external users via Trust Access tokens.'
+---
+
+
+ For internal organization users with full editing capabilities, see the main [Security Questionnaire](./security-questionnaire) documentation.
+
+
+## Overview
+
+The Security Questionnaire Trust Center enables external users with active Trust Access grants to automatically parse and answer security questionnaires using your organization's compliance documentation. This API endpoint provides a seamless, automated solution for completing vendor security questionnaires without requiring direct access to your internal systems.
+
+## 1. Key Concepts
+
+The Security Questionnaire Trust Center API consists of three core components:
+
+- **Trust Access Token:** A secure token obtained from an active Trust Access grant that authenticates external users
+- **Questionnaire Parsing:** AI-powered extraction of questions from uploaded questionnaire files (PDF, Excel, CSV)
+- **Automated Answer Generation:** Intelligent answer generation based on your organization's published policies and compliance documentation
+
+### Supported File Formats
+
+The API supports multiple questionnaire file formats:
+
+| Format | Extensions | Notes |
+| :---------- | :---------------------------- | :--------------------------------------- |
+| **PDF** | `.pdf` | Scanned documents and digital PDFs |
+| **Excel** | `.xlsx`, `.xls` | Spreadsheet-based questionnaires |
+| **CSV** | `.csv` | Comma-separated value files |
+
+### Output Formats
+
+Completed questionnaires can be exported in three formats:
+
+| Format | MIME Type | Use Case |
+| :----- | :----------------------------------------------------------- | :-------------------------------- |
+| **XLSX** | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | Default format, editable in Excel |
+| **PDF** | `application/pdf` | Final submission format |
+| **CSV** | `text/csv` | Simple data exchange |
+
+---
+
+## 2. Prerequisites
+
+Before using the Security Questionnaire Trust Center API, ensure the following:
+
+1. **Active Trust Access Grant:** You must have an active Trust Access grant with a valid access token
+ - Access grants are obtained through the Trust Access workflow (see [Trust Access documentation](./trust-access))
+ - The grant must be in `Active` status
+ - Access tokens are provided via email when access is granted
+
+2. **Questionnaire File:** Prepare your security questionnaire file in a supported format
+ - Ensure the file is readable and not corrupted
+ - For best results, use structured formats (Excel, CSV) when possible
+ - PDF files should have extractable text (not just scanned images)
+
+---
+
+## 3. API Endpoint
+
+### Endpoint Details
+
+**URL:** `/v1/questionnaire/parse/upload/token`
+
+**Method:** `POST`
+
+**Authentication:** Query parameter `token` (Trust Access token)
+
+**Content-Type:** `multipart/form-data`
+
+### Request Parameters
+
+#### Query Parameters
+
+| Parameter | Type | Required | Description |
+| :-------- | :------- | :------- | :--------------------------------------------- |
+| `token` | `string` | Yes | Trust Access token from your active grant |
+
+#### Form Data
+
+| Parameter | Type | Required | Description |
+| :-------- | :------------------------ | :------- | :--------------------------------------------- |
+| `file` | `File` | Yes | Questionnaire file (PDF, Excel, CSV) |
+| `format` | `'pdf' \| 'csv' \| 'xlsx'` | No | Output format (defaults to `xlsx`) |
+
+### Response
+
+**Content-Type:** Varies based on requested format:
+- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (XLSX)
+- `application/pdf` (PDF)
+- `text/csv` (CSV)
+
+**Headers:**
+- `Content-Type`: MIME type of the returned file
+- `Content-Disposition`: `attachment; filename="questionnaire.{format}"`
+- `X-Question-Count`: Number of questions extracted and answered
+
+**Body:** Binary file content (the completed questionnaire)
+
+---
+
+## 4. Workflow: Step-by-Step
+
+### Step 1: Obtain Trust Access Token
+
+1. Complete the Trust Access workflow to receive an active access grant
+2. Check your email for the "Access Granted" notification
+3. The access link email contains your Trust Access token
+4. Extract the token from the access link URL (the `token` query parameter)
+
+
+ Trust Access tokens are valid for the duration of your access grant (default 30 days, configurable 7-365 days). If your token expires, you'll need to request new access through the Trust Access workflow.
+
+
+### Step 2: Prepare Your Questionnaire File
+
+1. Ensure your questionnaire file is in a supported format
+2. Verify the file is readable and not corrupted
+3. For best results:
+ - Use structured formats (Excel, CSV) when available
+ - Ensure PDF files have extractable text
+
+### Step 3: Submit Questionnaire via API
+
+#### Using cURL
+
+```bash
+curl -X POST \
+ "https://api.trycomp.ai/v1/questionnaire/parse/upload/token?token=YOUR_TRUST_ACCESS_TOKEN" \
+ -F "file=@/path/to/questionnaire.pdf" \
+ -F "format=xlsx" \
+ --output completed-questionnaire.xlsx
+```
+
+#### Using JavaScript/TypeScript (Fetch API)
+
+```javascript
+const format = 'xlsx'; // or 'pdf' or 'csv'
+const formData = new FormData();
+formData.append('file', fileInput.files[0]);
+formData.append('format', format);
+
+const response = await fetch(
+ `https://api.trycomp.ai/v1/questionnaire/parse/upload/token?token=${trustAccessToken}`,
+ {
+ method: 'POST',
+ body: formData,
+ }
+);
+
+const blob = await response.blob();
+const questionCount = response.headers.get('X-Question-Count');
+
+// Download the file
+const url = window.URL.createObjectURL(blob);
+const a = document.createElement('a');
+a.href = url;
+a.download = `completed-questionnaire.${format}`;
+a.click();
+```
+
+#### Using Python (Requests)
+
+```python
+import requests
+
+url = "https://api.trycomp.ai/v1/questionnaire/parse/upload/token"
+params = {"token": "YOUR_TRUST_ACCESS_TOKEN"}
+files = {"file": open("questionnaire.pdf", "rb")}
+data = {"format": "xlsx"}
+
+response = requests.post(url, params=params, files=files, data=data)
+
+# Save the completed questionnaire
+with open("completed-questionnaire.xlsx", "wb") as f:
+ f.write(response.content)
+
+# Get question count
+question_count = response.headers.get("X-Question-Count")
+print(f"Processed {question_count} questions")
+```
+
+### Step 4: Process Response
+
+1. **Check Response Status:** Ensure the request returns `200 OK`
+2. **Download File:** Save the response body as a file with the appropriate extension
+3. **Review Question Count:** Check the `X-Question-Count` header to verify all questions were processed
+4. **Review Answers:** Open the downloaded file and review the generated answers
+
+### Step 5: Review and Submit
+
+1. **Review Generated Answers:** Open the completed questionnaire file
+2. **Verify Accuracy:** Check that answers align with your requirements
+3. **Edit if Needed:** Make any necessary manual edits to answers
+4. **Submit:** Use the completed questionnaire for your security assessment submission
+
+---
+
+## 5. How It Works
+
+### Question Extraction
+
+The API uses AI-powered parsing to extract questions from your uploaded file:
+
+- **Excel/CSV Files:** Uses advanced parsing algorithms optimized for structured data
+- **PDF Files:** Employs vision AI to extract text and identify question-answer pairs
+
+### Answer Generation
+
+Answers are automatically generated using:
+
+- **Published Policies:** Your organization's published compliance policies serve as the primary source
+- **Knowledge Base:** Additional documentation and context from the organization's knowledge base
+- **AI Analysis:** Advanced language models analyze questions and match them to relevant policy content
+
+### Processing Time
+
+Typical processing times vary by file type and size:
+
+| File Type | Average Processing Time | Notes |
+| :-------- | :---------------------- | :----------------------- |
+| **Excel** | 5-15 seconds | Fast parsing with Groq |
+| **CSV** | 5-10 seconds | Fast parsing with Groq |
+| **PDF** | 15-30 seconds | Vision AI processing |
+
+---
+
+## 6. Error Handling
+
+### Common Error Responses
+
+#### Invalid Token (401 Unauthorized)
+
+```json
+{
+ "statusCode": 401,
+ "message": "Invalid or expired access token",
+ "error": "Unauthorized"
+}
+```
+
+**Solution:** Verify your Trust Access token is valid and your access grant is still active.
+
+#### Missing File (400 Bad Request)
+
+```json
+{
+ "statusCode": 400,
+ "message": "file is required",
+ "error": "Bad Request"
+}
+```
+
+**Solution:** Ensure the `file` parameter is included in your form data.
+
+#### Unsupported File Format (400 Bad Request)
+
+```json
+{
+ "statusCode": 400,
+ "message": "Unsupported file type: application/octet-stream",
+ "error": "Bad Request"
+}
+```
+
+**Solution:** Use a supported file format (PDF, Excel, or CSV).
+
+#### File Too Large (413 Payload Too Large)
+
+```json
+{
+ "statusCode": 413,
+ "message": "File size exceeds maximum limit",
+ "error": "Payload Too Large"
+}
+```
+
+**Solution:** Reduce file size or split into multiple submissions.
+
+---
+
+## 7. Best Practices
+
+### For External Users
+
+1. **Use Structured Formats:** Prefer Excel or CSV formats when available for faster processing
+2. **Verify Token Validity:** Check that your Trust Access grant is still active before submitting
+3. **Review Generated Answers:** Always review AI-generated answers before final submission
+4. **Handle Errors Gracefully:** Implement proper error handling in your integration
+5. **Respect Rate Limits:** Avoid submitting multiple requests simultaneously
+
+### For Organizations
+
+1. **Keep Policies Updated:** Ensure published policies are current and comprehensive
+2. **Maintain Knowledge Base:** Keep additional documentation up to date
+3. **Monitor Usage:** Track API usage through Trust Access audit logs
+4. **Set Appropriate Grant Durations:** Configure access grant durations based on your needs
+
+---
+
+## 8. Integration Examples
+
+### Web Application Integration
+
+```html
+
+
+
+ Security Questionnaire Submission
+
+
+
+
+
+
+
+```
+
+### API Client Library (TypeScript)
+
+```typescript
+interface QuestionnaireSubmissionOptions {
+ token: string;
+ file: File | Blob;
+ format?: 'pdf' | 'csv' | 'xlsx';
+}
+
+interface QuestionnaireResponse {
+ file: Blob;
+ questionCount: number;
+ mimeType: string;
+ filename: string;
+}
+
+class SecurityQuestionnaireClient {
+ private baseUrl = 'https://api.trycomp.ai/v1';
+
+ async submitQuestionnaire(
+ options: QuestionnaireSubmissionOptions
+ ): Promise {
+ const formData = new FormData();
+ formData.append('file', options.file);
+ if (options.format) {
+ formData.append('format', options.format);
+ }
+
+ const response = await fetch(
+ `${this.baseUrl}/questionnaire/parse/upload/token?token=${options.token}`,
+ {
+ method: 'POST',
+ body: formData,
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || 'Failed to process questionnaire');
+ }
+
+ const blob = await response.blob();
+ const questionCount = parseInt(
+ response.headers.get('X-Question-Count') || '0',
+ 10
+ );
+
+ return {
+ file: blob,
+ questionCount,
+ mimeType: response.headers.get('Content-Type') || '',
+ filename: this.extractFilename(response.headers.get('Content-Disposition') || ''),
+ };
+ }
+
+ private extractFilename(contentDisposition: string): string {
+ const match = contentDisposition.match(/filename="(.+)"/);
+ return match ? match[1] : 'questionnaire.xlsx';
+ }
+}
+
+// Usage
+const client = new SecurityQuestionnaireClient();
+const result = await client.submitQuestionnaire({
+ token: 'your-trust-access-token',
+ file: fileInput.files[0],
+ format: 'xlsx',
+});
+
+// Download the file
+const url = URL.createObjectURL(result.file);
+const a = document.createElement('a');
+a.href = url;
+a.download = result.filename;
+a.click();
+```
+
+---
+
+## 9. Troubleshooting
+
+
+ ### Invalid Token Error
+
+ **Problem:** Receiving "Invalid or expired access token" error.
+
+ **Solutions:**
+ - Verify your Trust Access grant is still active
+ - Check that you're using the correct token from your access link
+ - Request new access if your grant has expired
+
+ ### Questions Not Extracted
+
+ **Problem:** Few or no questions extracted from the questionnaire.
+
+ **Solutions:**
+ - Ensure the file format is supported
+ - Verify the file is readable and not corrupted
+ - Try converting to a structured format (Excel/CSV) if possible
+ - Check that the file contains actual questionnaire content
+
+ ### Answers Not Accurate
+
+ **Problem:** Generated answers don't match expectations.
+
+ **Solutions:**
+ - This is expected - answers are based on the organization's published policies
+ - Review and edit answers manually before submission
+ - Contact the organization if you need clarification on specific answers
+
+ ### File Upload Fails
+
+ **Problem:** File upload returns an error.
+
+ **Solutions:**
+ - Check file size (must be under 10MB)
+ - Verify file format is supported
+ - Ensure proper Content-Type headers are set
+ - Try a different file format if issues persist
+
+
+---
+
+## 10. Security Considerations
+
+### Token Security
+
+- **Never Share Tokens:** Keep your Trust Access token confidential
+- **Use HTTPS:** Always use HTTPS when making API requests
+- **Token Expiration:** Tokens expire with your access grant - request new access when needed
+- **Revocation:** Organizations can revoke access at any time, invalidating tokens
+
+### Data Privacy
+
+- **File Content:** Questionnaire files are processed securely and stored temporarily
+- **Answer Generation:** Answers are generated based on published policies only
+- **Audit Trail:** All API usage is logged for security and compliance purposes
+
+---
+
+## 11. Support
+
+For additional assistance with the Security Questionnaire Trust Center API:
+
+1. **Documentation:** Review the [Trust Access documentation](./trust-access) for token management
+2. **API Reference:** Check the [API documentation](/api) for detailed endpoint specifications
+3. **Support:** Contact support at [support@trycomp.ai](mailto:support@trycomp.ai)
+4. **Community:** Join our [Discord community](https://discord.gg/compai) for peer support
+
diff --git a/packages/docs/security-questionnaire.mdx b/packages/docs/security-questionnaire.mdx
index b465e3eea..a0ff23f9b 100644
--- a/packages/docs/security-questionnaire.mdx
+++ b/packages/docs/security-questionnaire.mdx
@@ -3,6 +3,10 @@ title: "Security Questionnaire"
description: "Automatically answer security questionnaires using AI-powered analysis of your organization's policies and documentation"
---
+
+ For external users accessing via Trust Access tokens, see [Security Questionnaire](./security-questionnaire-trust-center) in the Trust section.
+
+
### About Security Questionnaire
The Security Questionnaire feature allows you to automatically analyze and answer vendor security questionnaires using AI. Upload questionnaires from vendors, and our system will extract questions and generate answers based on your organization's published policies and documentation.
diff --git a/packages/docs/trust-access.mdx b/packages/docs/trust-access.mdx
index 4b8a3e326..cd790575e 100644
--- a/packages/docs/trust-access.mdx
+++ b/packages/docs/trust-access.mdx
@@ -1,5 +1,5 @@
---
-title: 'Trust Access'
+title: 'Access'
description: 'A comprehensive guide to managing external access requests, NDAs, and approvals.'
---
@@ -130,10 +130,27 @@ If the 7-day signing window expires, administrators see `NDA Link Expired` statu
After successfully signing the NDA, users receive an email notification: _"Access Granted"_. This email contains their first **Access Link** (valid for 24 hours).
-Once authenticated via the access link, users can:
+Once authenticated via the access link, users are presented with a tabbed interface providing access to different features:
-- Browse and read all published, non-archived compliance policies
-- Generate a single PDF bundle containing all accessible policies
+
+
+#### Documents Tab (Default)
+
+The **Documents** tab is selected by default and provides access to compliance documentation:
+
+- **Compliance Resources Section:** Browse and read all published, non-archived compliance policies organized by category
+
+
+
+- **PDF Bundle Generation:** Generate a single PDF bundle containing all accessible policies
- The downloaded PDF bundle is watermarked with the user's full name, email address, and a unique document identifier
-Access grant status shows as `Active` in the dashboard, the grant expiration date is visible, and download activity is logged when users generate PDF bundles.
+#### Security Questionnaire Tab
+
+
+
+The **Security Questionnaire** tab enables external users to automatically parse and answer security questionnaires using the organization's compliance documentation.
+
+**Questionnaire Upload Interface:**
+
+1. **File Upload Field:** Upload your security questionnaire file
+ - Supported formats: PDF, CSV, Excel (.xlsx, .xls)
+ - File must be readable and not corrupted
+ - Maximum file size: 10MB
+
+2. **Output Format Selection:** Choose the format for the processed questionnaire
+ - **Excel (.xlsx)** - Default format, editable in Excel
+ - **PDF** - Final submission format
+ - **CSV** - Simple data exchange format
+
+3. **Upload & Process Button:** Submit the questionnaire for processing
+ - The system automatically extracts questions from your file
+ - Answers are generated based on the organization's published policies
+ - Processing typically takes 5-30 seconds depending on file type and size
+
+After processing, the completed questionnaire is automatically downloaded with all questions answered based on the organization's compliance documentation. Users can review and edit answers before final submission.
+
+
+ For detailed API documentation and integration examples, see the [Security Questionnaire](./security-questionnaire-trust-center) documentation in the Trust section.
+
+
+Access grant status shows as `Active` in the dashboard, the grant expiration date is visible, and all activity (document downloads and questionnaire submissions) is logged for audit purposes.
---