Skip to content

Commit a686844

Browse files
committed
chore: merge main into release for new releases
2 parents 42a247a + caef0d9 commit a686844

50 files changed

Lines changed: 2223 additions & 1105 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Maced contract canary
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'apps/api/src/security-penetration-tests/**'
7+
- 'apps/api/test/maced-contract.e2e-spec.ts'
8+
- 'apps/api/package.json'
9+
- '.github/workflows/maced-contract-canary.yml'
10+
schedule:
11+
- cron: '0 * * * *'
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
maced-contract-canary:
19+
runs-on: warp-ubuntu-latest-arm64-4x
20+
timeout-minutes: 15
21+
env:
22+
MACED_API_KEY: ${{ secrets.MACED_API_KEY }}
23+
MACED_CONTRACT_E2E_RUN_ID: ${{ secrets.MACED_CONTRACT_E2E_RUN_ID }}
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: ./.github/actions/dangerous-git-checkout
27+
- name: Install Bun
28+
uses: oven-sh/setup-bun@v2
29+
with:
30+
bun-version: latest
31+
- name: Install dependencies
32+
run: bun install --frozen-lockfile
33+
- name: Run Maced provider contract canary
34+
working-directory: ./apps/api
35+
run: bun run test:e2e:maced

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"test:cov": "jest --coverage",
127127
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
128128
"test:e2e": "jest --config ./test/jest-e2e.json",
129+
"test:e2e:maced": "MACED_CONTRACT_E2E=1 jest --config ./test/jest-e2e.json --runInBand ./maced-contract.e2e-spec.ts",
129130
"test:watch": "jest --watch",
130131
"typecheck": "tsc --noEmit"
131132
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as React from 'react';
2+
import {
3+
Body,
4+
Button,
5+
Container,
6+
Font,
7+
Heading,
8+
Html,
9+
Link,
10+
Preview,
11+
Section,
12+
Tailwind,
13+
Text,
14+
} from '@react-email/components';
15+
import { Footer } from '../components/footer';
16+
import { Logo } from '../components/logo';
17+
import { getUnsubscribeUrl } from '@trycompai/email';
18+
19+
interface FailedTaskItem {
20+
title: string;
21+
url: string;
22+
failedCount: number;
23+
totalCount: number;
24+
}
25+
26+
interface Props {
27+
toName: string;
28+
toEmail: string;
29+
organizationName: string;
30+
tasksUrl: string;
31+
tasks: FailedTaskItem[];
32+
}
33+
34+
const MAX_DISPLAYED_TASKS = 15;
35+
36+
export const AutomationBulkFailuresEmail = ({
37+
toName,
38+
toEmail,
39+
organizationName,
40+
tasksUrl,
41+
tasks,
42+
}: Props) => {
43+
const unsubscribeUrl = getUnsubscribeUrl(toEmail);
44+
const taskCount = tasks.length;
45+
const taskText = taskCount === 1 ? 'task' : 'tasks';
46+
const displayedTasks = tasks.slice(0, MAX_DISPLAYED_TASKS);
47+
const remainingCount = taskCount - displayedTasks.length;
48+
49+
return (
50+
<Html>
51+
<Tailwind>
52+
<head>
53+
<Font
54+
fontFamily="Geist"
55+
fallbackFontFamily="Helvetica"
56+
fontWeight={400}
57+
fontStyle="normal"
58+
/>
59+
<Font
60+
fontFamily="Geist"
61+
fallbackFontFamily="Helvetica"
62+
fontWeight={500}
63+
fontStyle="normal"
64+
/>
65+
</head>
66+
<Preview>
67+
{`${taskCount} ${taskText} with automation failures`}
68+
</Preview>
69+
70+
<Body className="mx-auto my-auto bg-[#fff] font-sans">
71+
<Container
72+
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
73+
style={{ borderStyle: 'solid', borderWidth: 1 }}
74+
>
75+
<Logo />
76+
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
77+
Automation Failures Summary
78+
</Heading>
79+
80+
<Text className="text-[14px] leading-[24px] text-[#121212]">
81+
Hello {toName},
82+
</Text>
83+
84+
<Text className="text-[14px] leading-[24px] text-[#121212]">
85+
Today's scheduled automations found failures in{' '}
86+
<strong>{taskCount}</strong> {taskText} in{' '}
87+
<strong>{organizationName}</strong>.
88+
</Text>
89+
90+
<Section className="mt-[16px] mb-[16px]">
91+
{displayedTasks.map((task, index) => (
92+
<Text key={index} className="my-[4px] text-[14px] leading-[24px] text-[#121212]">
93+
{'• '}
94+
<Link href={task.url} className="text-[#121212] underline">
95+
{task.title}
96+
</Link>
97+
{' '}({task.failedCount}/{task.totalCount} failed)
98+
</Text>
99+
))}
100+
{remainingCount > 0 && (
101+
<Text className="my-[4px] text-[14px] leading-[24px] text-[#666666]">
102+
and {remainingCount} more...
103+
</Text>
104+
)}
105+
</Section>
106+
107+
<Section className="mt-[32px] mb-[32px] text-center">
108+
<Button
109+
className="rounded-[3px] bg-[#121212] px-[20px] py-[12px] text-center text-[14px] font-semibold text-white no-underline"
110+
href={tasksUrl}
111+
>
112+
View Tasks
113+
</Button>
114+
</Section>
115+
116+
<Text className="text-[14px] leading-[24px] text-[#121212]">
117+
or copy and paste this URL into your browser:{' '}
118+
<a href={tasksUrl} className="text-[#121212] underline">
119+
{tasksUrl}
120+
</a>
121+
</Text>
122+
123+
<Section className="mt-[30px] mb-[20px]">
124+
<Text className="text-[12px] leading-[20px] text-[#666666]">
125+
Don't want to receive task assignment notifications?{' '}
126+
<Link href={unsubscribeUrl} className="text-[#121212] underline">
127+
Manage your email preferences
128+
</Link>
129+
.
130+
</Text>
131+
</Section>
132+
133+
<br />
134+
135+
<Footer />
136+
</Container>
137+
</Body>
138+
</Tailwind>
139+
</Html>
140+
);
141+
};
142+
143+
export default AutomationBulkFailuresEmail;

apps/api/src/evidence-forms/evidence-forms.controller.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AuthContext as AuthContextType } from '@/auth/types';
44
import {
55
Body,
66
Controller,
7+
Delete,
78
Get,
89
Header,
910
Param,
@@ -127,6 +128,26 @@ export class EvidenceFormsController {
127128
});
128129
}
129130

131+
@Delete(':formType/submissions/:submissionId')
132+
@ApiOperation({
133+
summary: 'Delete a submission',
134+
description:
135+
'Remove an evidence form submission for the active organization. Requires owner, admin, or auditor role.',
136+
})
137+
async deleteSubmission(
138+
@OrganizationId() organizationId: string,
139+
@AuthContext() authContext: AuthContextType,
140+
@Param('formType') formType: string,
141+
@Param('submissionId') submissionId: string,
142+
) {
143+
return this.evidenceFormsService.deleteSubmission({
144+
organizationId,
145+
authContext,
146+
formType,
147+
submissionId,
148+
});
149+
}
150+
130151
@Post(':formType/submissions')
131152
@ApiOperation({
132153
summary: 'Submit evidence form entry',

apps/api/src/evidence-forms/evidence-forms.service.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const reviewSchema = z.object({
4646
});
4747

4848
const EVIDENCE_FORM_REVIEWER_ROLES = ['owner', 'admin', 'auditor'] as const;
49+
const EVIDENCE_FORM_DELETE_ROLES = ['owner', 'admin'] as const;
4950
const MAX_UPLOAD_FILE_SIZE_BYTES = 100 * 1024 * 1024;
5051
const MAX_UPLOAD_BASE64_LENGTH = Math.ceil(MAX_UPLOAD_FILE_SIZE_BYTES / 3) * 4;
5152

@@ -159,6 +160,20 @@ export class EvidenceFormsService {
159160
return userId;
160161
}
161162

163+
private requireEvidenceDeleteAccess(authContext: AuthContext): string {
164+
const userId = this.requireJwtUser(authContext);
165+
const roles = authContext.userRoles ?? [];
166+
const canDelete = EVIDENCE_FORM_DELETE_ROLES.some((role) => roles.includes(role));
167+
168+
if (!canDelete) {
169+
throw new UnauthorizedException(
170+
`Delete denied. Required one of roles: ${EVIDENCE_FORM_DELETE_ROLES.join(', ')}`,
171+
);
172+
}
173+
174+
return userId;
175+
}
176+
162177
private decodeBase64File(fileData: string): Buffer {
163178
const normalized = fileData.trim();
164179
if (normalized.length === 0 || normalized.length % 4 !== 0) {
@@ -315,6 +330,38 @@ export class EvidenceFormsService {
315330
};
316331
}
317332

333+
async deleteSubmission(params: {
334+
organizationId: string;
335+
authContext: AuthContext;
336+
formType: string;
337+
submissionId: string;
338+
}) {
339+
this.requireEvidenceDeleteAccess(params.authContext);
340+
341+
const parsedType = evidenceFormTypeSchema.safeParse(params.formType);
342+
if (!parsedType.success) {
343+
throw new BadRequestException('Unsupported form type');
344+
}
345+
346+
const submission = await db.evidenceSubmission.findFirst({
347+
where: {
348+
id: params.submissionId,
349+
organizationId: params.organizationId,
350+
formType: toDbEvidenceFormType(parsedType.data),
351+
},
352+
});
353+
354+
if (!submission) {
355+
throw new NotFoundException('Submission not found');
356+
}
357+
358+
await db.evidenceSubmission.delete({
359+
where: { id: params.submissionId },
360+
});
361+
362+
return { success: true, id: params.submissionId };
363+
}
364+
318365
async submitForm(params: {
319366
organizationId: string;
320367
formType: string;

apps/api/src/security-penetration-tests/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,14 @@ This module exposes Comp API endpoints under `/v1/security-penetration-tests` an
3838

3939
- Frontend should call Nest API only (no Next.js proxy routes for this feature).
4040
- Provider callbacks to non-Comp webhook URLs are passed through and are not forced to include Comp-specific webhook tokens.
41+
42+
## Maced contract canary test (real provider)
43+
44+
Use this e2e canary to detect Maced API contract drift against the live provider without creating new paid runs.
45+
46+
- Test file: `apps/api/test/maced-contract.e2e-spec.ts`
47+
- Command:
48+
- `MACED_API_KEY=<key> bun run test:e2e:maced`
49+
- Optional deep-check env:
50+
- `MACED_CONTRACT_E2E_RUN_ID=<existing_provider_run_id>`
51+
- When present, the test also calls `GET /v1/pentests/:id` and `GET /v1/pentests/:id/progress`.

apps/api/src/security-penetration-tests/dto/create-penetration-test.dto.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ export class CreatePenetrationTestDto {
5151
@IsString()
5252
workspace?: string;
5353

54-
@ApiPropertyOptional({
55-
description:
56-
'Set false to reject non-mocked checkout flows for strict behavior',
57-
required: false,
58-
default: true,
59-
})
60-
@IsOptional()
61-
@IsBoolean()
62-
mockCheckout?: boolean;
63-
6454
@ApiPropertyOptional({
6555
description: 'Optional webhook URL to notify when report generation completes',
6656
required: false,

0 commit comments

Comments
 (0)