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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ APP_AWS_ACCESS_KEY_ID="" # AWS Access Key ID
APP_AWS_SECRET_ACCESS_KEY="" # AWS Secret Access Key
APP_AWS_REGION="" # AWS Region
APP_AWS_BUCKET_NAME="" # AWS Bucket Name
APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET="" # AWS, Required for Security Questionnaire feature

TRIGGER_SECRET_KEY="" # For background jobs. Self-host or use cloud-version @ https://trigger.dev
# TRIGGER_API_URL="" # Only set if you are self-hosting
Expand All @@ -26,4 +27,6 @@ TRIGGER_SECRET_KEY="" # Secret key from Trigger.dev
OPENAI_API_KEY="" # AI Chat + Auto Generated Policies, Risks + Vendors
FIRECRAWL_API_KEY="" # For research, self-host or use cloud-version @ https://firecrawl.dev

TRUST_APP_URL="http://localhost:3008" # Trust portal public site for NDA signing and access requests

AUTH_TRUSTED_ORIGINS=http://localhost:3000,https://*.trycomp.ai,http://localhost:3002
8 changes: 5 additions & 3 deletions .github/workflows/auto-pr-to-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ on:
- claudio/*
- mariano/*
- lewis/*
- daniel/*
- COMP-*
- cursor/*
- codex/*
- chas/*
- tofik/*
jobs:
create-pull-request:
runs-on: warp-ubuntu-latest-arm64-4x
Expand All @@ -35,9 +37,9 @@ jobs:
uses: repo-sync/pull-request@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
destination_branch: 'main'
pr_title: '[dev] [${{ github.actor }}] ${{ github.ref_name }}'
pr_label: 'automated-pr'
destination_branch: "main"
pr_title: "[dev] [${{ github.actor }}] ${{ github.ref_name }}"
pr_label: "automated-pr"
pr_body: |
This is an automated pull request to merge ${{ github.ref_name }} into dev.
It was created by the [Auto Pull Request] action.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ node_modules
.pnp
.pnp.js

.idea/
# testing
coverage

Expand Down Expand Up @@ -83,4 +84,5 @@ packages/*/dist
**/src/db/generated/

# Release script
scripts/sync-release-branch.sh
scripts/sync-release-branch.sh
/.vscode
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
},
"typescript.tsserver.experimental.enableProjectDiagnostics": true
}
8 changes: 8 additions & 0 deletions SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Portal (`apps/portal`):

App (`apps/app`):

- **APP_AWS_REGION**, **APP_AWS_ACCESS_KEY_ID**, **APP_AWS_SECRET_ACCESS_KEY**, **APP_AWS_BUCKET_NAME**: AWS S3 credentials for file storage (attachments, general uploads).
- **APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET**: AWS S3 bucket name specifically for questionnaire file uploads. Required for the Security Questionnaire feature. If not set, users will see an error when trying to parse questionnaires.
- **OPENAI_API_KEY**: Enables AI features that call OpenAI models.
- **UPSTASH_REDIS_REST_URL**, **UPSTASH_REDIS_REST_TOKEN**: Optional Redis (Upstash) used for rate limiting/queues/caching.
- **NEXT_PUBLIC_POSTHOG_KEY**, **NEXT_PUBLIC_POSTHOG_HOST**: Client analytics via PostHog; leave unset to disable.
Expand Down Expand Up @@ -143,6 +145,12 @@ BETTER_AUTH_URL_PORTAL=http://localhost:3002
NEXT_PUBLIC_BETTER_AUTH_URL_PORTAL=http://localhost:3002

# Optional
# AWS S3 (for file storage)
# APP_AWS_REGION=
# APP_AWS_ACCESS_KEY_ID=
# APP_AWS_SECRET_ACCESS_KEY=
# APP_AWS_BUCKET_NAME=
# APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET=
# OPENAI_API_KEY=
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=
Expand Down
190 changes: 190 additions & 0 deletions apps/.cursor/rules/trigger.basic.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
description: Only the most important rules for writing basic Trigger.dev tasks
globs: **/trigger/**/*.ts
alwaysApply: false
---
# Trigger.dev Basic Tasks (v4)

**MUST use `@trigger.dev/sdk` (v4), NEVER `client.defineJob`**

## Basic Task

```ts
import { task } from "@trigger.dev/sdk";

export const processData = task({
id: "process-data",
retry: {
maxAttempts: 10,
factor: 1.8,
minTimeoutInMs: 500,
maxTimeoutInMs: 30_000,
randomize: false,
},
run: async (payload: { userId: string; data: any[] }) => {
// Task logic - runs for long time, no timeouts
console.log(`Processing ${payload.data.length} items for user ${payload.userId}`);
return { processed: payload.data.length };
},
});
```

## Schema Task (with validation)

```ts
import { schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";

export const validatedTask = schemaTask({
id: "validated-task",
schema: z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
}),
run: async (payload) => {
// Payload is automatically validated and typed
return { message: `Hello ${payload.name}, age ${payload.age}` };
},
});
```

## Scheduled Task

```ts
import { schedules } from "@trigger.dev/sdk";

const dailyReport = schedules.task({
id: "daily-report",
cron: "0 9 * * *", // Daily at 9:00 AM UTC
// or with timezone: cron: { pattern: "0 9 * * *", timezone: "America/New_York" },
run: async (payload) => {
console.log("Scheduled run at:", payload.timestamp);
console.log("Last run was:", payload.lastTimestamp);
console.log("Next 5 runs:", payload.upcoming);

// Generate daily report logic
return { reportGenerated: true, date: payload.timestamp };
},
});
```

## Triggering Tasks

### From Backend Code

```ts
import { tasks } from "@trigger.dev/sdk";
import type { processData } from "./trigger/tasks";

// Single trigger
const handle = await tasks.trigger<typeof processData>("process-data", {
userId: "123",
data: [{ id: 1 }, { id: 2 }],
});

// Batch trigger
const batchHandle = await tasks.batchTrigger<typeof processData>("process-data", [
{ payload: { userId: "123", data: [{ id: 1 }] } },
{ payload: { userId: "456", data: [{ id: 2 }] } },
]);
```

### From Inside Tasks (with Result handling)

```ts
export const parentTask = task({
id: "parent-task",
run: async (payload) => {
// Trigger and continue
const handle = await childTask.trigger({ data: "value" });

// Trigger and wait - returns Result object, NOT task output
const result = await childTask.triggerAndWait({ data: "value" });
if (result.ok) {
console.log("Task output:", result.output); // Actual task return value
} else {
console.error("Task failed:", result.error);
}

// Quick unwrap (throws on error)
const output = await childTask.triggerAndWait({ data: "value" }).unwrap();

// Batch trigger and wait
const results = await childTask.batchTriggerAndWait([
{ payload: { data: "item1" } },
{ payload: { data: "item2" } },
]);

for (const run of results) {
if (run.ok) {
console.log("Success:", run.output);
} else {
console.log("Failed:", run.error);
}
}
},
});

export const childTask = task({
id: "child-task",
run: async (payload: { data: string }) => {
return { processed: payload.data };
},
});
```

> Never wrap triggerAndWait or batchTriggerAndWait calls in a Promise.all or Promise.allSettled as this is not supported in Trigger.dev tasks.

## Waits

```ts
import { task, wait } from "@trigger.dev/sdk";

export const taskWithWaits = task({
id: "task-with-waits",
run: async (payload) => {
console.log("Starting task");

// Wait for specific duration
await wait.for({ seconds: 30 });
await wait.for({ minutes: 5 });
await wait.for({ hours: 1 });
await wait.for({ days: 1 });

// Wait until specific date
await wait.until({ date: new Date("2024-12-25") });

// Wait for token (from external system)
await wait.forToken({
token: "user-approval-token",
timeoutInSeconds: 3600, // 1 hour timeout
});

console.log("All waits completed");
return { status: "completed" };
},
});
```

> Never wrap wait calls in a Promise.all or Promise.allSettled as this is not supported in Trigger.dev tasks.

## Key Points

- **Result vs Output**: `triggerAndWait()` returns a `Result` object with `ok`, `output`, `error` properties - NOT the direct task output
- **Type safety**: Use `import type` for task references when triggering from backend
- **Waits > 5 seconds**: Automatically checkpointed, don't count toward compute usage

## NEVER Use (v2 deprecated)

```ts
// BREAKS APPLICATION
client.defineJob({
id: "job-id",
run: async (payload, io) => {
/* ... */
},
});
```

Use v4 SDK (`@trigger.dev/sdk`), check `result.ok` before accessing `result.output`
1 change: 1 addition & 0 deletions apps/api/buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ phases:
- '[ -n "$DATABASE_URL" ] || { echo "❌ DATABASE_URL is not set"; exit 1; }'
- '[ -n "$BASE_URL" ] || { echo "❌ BASE_URL is not set"; exit 1; }'
- '[ -n "$BETTER_AUTH_URL" ] || { echo "❌ BETTER_AUTH_URL is not set"; exit 1; }'
- '[ -n "$TRUST_APP_URL" ] || { echo "❌ TRUST_APP_URL is not set"; exit 1; }'

# Install only API workspace dependencies
- echo "Installing API dependencies only..."
Expand Down
4 changes: 4 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
"class-validator": "^0.14.2",
"dotenv": "^17.2.3",
"jose": "^6.0.12",
"jspdf": "^3.0.3",
"nanoid": "^5.1.6",
"pdf-lib": "^1.17.1",
"prisma": "^6.13.0",
"reflect-metadata": "^0.2.2",
"resend": "^6.4.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
"zod": "^4.0.14"
Expand Down
58 changes: 55 additions & 3 deletions apps/api/src/attachments/attachments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,60 @@ export class AttachmentsService {
});
}

/**
* Sanitize filename for S3 storage
*/
async uploadToS3(
fileBuffer: Buffer,
fileName: string,
contentType: string,
organizationId: string,
entityType: string,
entityId: string,
): Promise<string> {
const fileId = randomBytes(16).toString('hex');
const sanitizedFileName = this.sanitizeFileName(fileName);
const timestamp = Date.now();
const s3Key = `${organizationId}/attachments/${entityType}/${entityId}/${timestamp}-${fileId}-${sanitizedFileName}`;

const putCommand = new PutObjectCommand({
Bucket: this.bucketName,
Key: s3Key,
Body: fileBuffer,
ContentType: contentType,
Metadata: {
originalFileName: this.sanitizeHeaderValue(fileName),
organizationId,
entityId,
entityType,
},
});

await this.s3Client.send(putCommand);
return s3Key;
}

async getPresignedDownloadUrl(s3Key: string): Promise<string> {
return this.generateSignedUrl(s3Key);
}

async getObjectBuffer(s3Key: string): Promise<Buffer> {
const getCommand = new GetObjectCommand({
Bucket: this.bucketName,
Key: s3Key,
});

const response = await this.s3Client.send(getCommand);
const chunks: Uint8Array[] = [];

if (!response.Body) {
throw new InternalServerErrorException('No file data received from S3');
}

for await (const chunk of response.Body as any) {
chunks.push(chunk);
}

return Buffer.concat(chunks);
}

private sanitizeFileName(fileName: string): string {
return fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
}
Expand All @@ -281,6 +332,7 @@ export class AttachmentsService {
* - Trim whitespace
*/
private sanitizeHeaderValue(value: string): string {
// eslint-disable-next-line no-control-regex
const withoutControls = value.replace(/[\x00-\x1F\x7F]/g, '');
const asciiOnly = withoutControls.replace(/[^\x20-\x7E]/g, '_');
return asciiOnly.trim();
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/comments/dto/update-comment.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export class UpdateCommentDto {
@IsNotEmpty()
@MaxLength(2000)
content: string;
}
}
Loading
Loading