Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ export class OAuthController {
};

// Add client credentials based on auth method
// Per OAuth 2.0 RFC 6749 Section 2.3.1, when using HTTP Basic auth (header),
// client credentials should NOT be included in the request body
if (config.clientAuthMethod === 'header') {
const creds = Buffer.from(
`${credentials.clientId}:${credentials.clientSecret}`,
Expand All @@ -422,8 +424,10 @@ export class OAuthController {
});

if (!response.ok) {
await response.text(); // consume body
this.logger.error(`Token exchange failed: ${response.status}`);
const errorBody = await response.text();
this.logger.error(
`Token exchange failed: ${response.status} - ${errorBody}`,
);
throw new Error(`Token exchange failed: ${response.status}`);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class SyncController {
}
const errorText = await response.text();
this.logger.error(
`Google API error: ${response.status} ${response.statusText}`,
`Google API error: ${response.status} ${response.statusText} - ${errorText}`,
);
throw new HttpException(
'Failed to fetch users from Google Workspace',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,18 @@ export class VariablesController {

fetch: async <T = unknown>(path: string): Promise<T> => {
const url = new URL(path, baseUrl);

const response = await fetch(url.toString(), {
headers: buildHeaders(),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();

if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}

const data = await response.json();
return data as T;
},

fetchAllPages: async <T = unknown>(path: string): Promise<T[]> => {
Expand All @@ -268,10 +275,30 @@ export class VariablesController {
const response = await fetch(url.toString(), {
headers: buildHeaders(),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);

const items: T[] = await response.json();
if (!Array.isArray(items) || items.length === 0) break;
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}

const data = await response.json();

// Handle both direct array responses and wrapped responses
// e.g., some APIs return { items: [...] } instead of [...]
let items: T[];
if (Array.isArray(data)) {
items = data;
} else if (data && typeof data === 'object') {
// Find the first array property in the response
const arrayValue = Object.values(data).find((v) =>
Array.isArray(v),
) as T[] | undefined;
items = arrayValue ?? [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-array heuristic may select wrong data in pagination

Low Severity

The fetchAllPages heuristic uses Object.values(data).find(v => Array.isArray(v)) to find data in wrapped responses. This selects the first array property by definition order, which could pick an unintended array if the response contains multiple arrays (e.g., { errors: [], items: [...] } would select the empty errors array). This causes pagination to terminate early with items.length === 0, silently returning no data. This could cause security checks to incorrectly pass if they find no resources to evaluate.

Fix in Cursor Fix in Web

} else {
items = [];
}

if (items.length === 0) break;

allItems.push(...items);
if (items.length < perPage) break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ export class CredentialVaultService {
};

// Add client credentials based on auth method
// Per OAuth 2.0 RFC 6749 Section 2.3.1, when using HTTP Basic auth (header),
// client credentials should NOT be included in the request body
if (config.clientAuthMethod === 'header') {
const credentials = Buffer.from(
`${config.clientId}:${config.clientSecret}`,
Expand Down
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@types/canvas-confetti": "^1.9.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/three": "^0.180.0",
"@uiw/react-json-view": "^2.0.0-alpha.40",
"@uploadthing/react": "^7.3.0",
"@upstash/ratelimit": "^2.0.5",
"@vercel/analytics": "^1.5.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function PolicyPage({
policyId,
organizationId,
logs,
showAiAssistant,
}: {
policy: (Policy & { approver: (Member & { user: User }) | null }) | null;
assignees: (Member & { user: User })[];
Expand All @@ -25,6 +26,8 @@ export default function PolicyPage({
/** Organization ID - required for correct org context in comments */
organizationId: string;
logs: AuditLogWithRelations[];
/** Whether the AI assistant feature is enabled */
showAiAssistant: boolean;
}) {
return (
<>
Expand All @@ -41,6 +44,7 @@ export default function PolicyPage({
policyContent={policy?.content ? (policy.content as JSONContent[]) : []}
displayFormat={policy?.displayFormat}
pdfUrl={policy?.pdfUrl}
aiAssistantEnabled={showAiAssistant}
/>

<RecentAuditLogs logs={logs} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ interface PolicyContentManagerProps {
isPendingApproval: boolean;
displayFormat?: PolicyDisplayFormat;
pdfUrl?: string | null;
/** Whether the AI assistant feature is enabled (behind feature flag) */
aiAssistantEnabled?: boolean;
}

export function PolicyContentManager({
Expand All @@ -89,8 +91,9 @@ export function PolicyContentManager({
isPendingApproval,
displayFormat = 'EDITOR',
pdfUrl,
aiAssistantEnabled = false,
}: PolicyContentManagerProps) {
const [showAiAssistant, setShowAiAssistant] = useState(true);
const [showAiAssistant, setShowAiAssistant] = useState(aiAssistantEnabled);
const [editorKey, setEditorKey] = useState(0);
const [currentContent, setCurrentContent] = useState<Array<JSONContent>>(() => {
const formattedContent = Array.isArray(policyContent)
Expand Down Expand Up @@ -205,7 +208,7 @@ export function PolicyContentManager({
PDF View
</TabsTrigger>
</TabsList>
{!isPendingApproval && (
{!isPendingApproval && aiAssistantEnabled && (
<Button
variant={showAiAssistant ? 'default' : 'outline'}
size="sm"
Expand Down Expand Up @@ -236,7 +239,7 @@ export function PolicyContentManager({
</Tabs>
</div>

{showAiAssistant && (
{aiAssistantEnabled && showAiAssistant && (
<div className="w-80 shrink-0 self-stretch flex flex-col overflow-hidden">
<PolicyAiAssistant
messages={messages}
Expand Down
13 changes: 13 additions & 0 deletions apps/app/src/app/(app)/[orgId]/policies/[policyId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { getFeatureFlags } from '@/app/posthog';
import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb';
import { auth } from '@/utils/auth';
import type { Metadata } from 'next';
import { headers } from 'next/headers';
import { PolicyHeaderActions } from './components/PolicyHeaderActions';
import PolicyPage from './components/PolicyPage';
import { getAssignees, getLogsForPolicy, getPolicy, getPolicyControlMappingInfo } from './data';
Expand All @@ -18,6 +21,15 @@ export default async function PolicyDetails({

const isPendingApproval = !!policy?.approverId;

// Check feature flag for AI policy editor
const session = await auth.api.getSession({
headers: await headers(),
});
const flags = session?.user?.id ? await getFeatureFlags(session.user.id) : {};
const isAiPolicyEditorEnabled =
flags['is-ai-policy-assistant-enabled'] === true ||
flags['is-ai-policy-assistant-enabled'] === 'true';

return (
<PageWithBreadcrumb
breadcrumbs={[
Expand All @@ -35,6 +47,7 @@ export default async function PolicyDetails({
allControls={allControls}
isPendingApproval={isPendingApproval}
logs={logs}
showAiAssistant={isAiPolicyEditorEnabled}
/>
</PageWithBreadcrumb>
);
Expand Down
Loading
Loading