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
5 changes: 0 additions & 5 deletions .changeset/chat-idle-wait-traces.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/outer-compaction-last-step-usage.md

This file was deleted.

29 changes: 19 additions & 10 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ on:
branches:
- main
paths:
- ".github/workflows/docs.yml"
- "docs/**"
pull_request:
types: [opened, synchronize, reopened]
paths:
- ".github/workflows/docs.yml"
- "docs/**"

concurrency:
Expand All @@ -30,15 +32,22 @@ jobs:
with:
persist-credentials: false

- name: 📦 Cache npm
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.npm
key: |
${{ runner.os }}-mintlify
restore-keys: |
${{ runner.os }}-mintlify
- name: 📦 Install Mintlify
working-directory: ${{ runner.temp }}
env:
COREPACK_HOME: ${{ runner.temp }}/mintlify-corepack
NPM_CONFIG_CACHE: ${{ runner.temp }}/mintlify-npm-cache
NPM_CONFIG_GLOBALCONFIG: /dev/null
NPM_CONFIG_USERCONFIG: /dev/null
PNPM_HOME: ${{ runner.temp }}/mintlify-pnpm
run: |
mkdir -p "$RUNNER_TEMP/mintlify-tool"
printf '{"private":true}\n' >"$RUNNER_TEMP/mintlify-tool/package.json"
corepack pnpm@10.33.2 --dir "$RUNNER_TEMP/mintlify-tool" add \
--save-exact \
--store-dir "$RUNNER_TEMP/mintlify-pnpm-store" \
--registry=https://registry.npmjs.org/ \
mintlify@4.0.393

- name: 🔗 Check for broken links
run: npx mintlify@4.0.393 broken-links
run: '"$RUNNER_TEMP/mintlify-tool/node_modules/.bin/mintlify" broken-links'
6 changes: 0 additions & 6 deletions .server-changes/billing-limit-banner-no-limit-button.md

This file was deleted.

6 changes: 0 additions & 6 deletions .server-changes/invite-email-case-insensitive.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { OperatingSystemContextProvider } from "~/components/primitives/OperatingSystemProvider";
import { ShortcutsProvider } from "~/components/primitives/ShortcutsProvider";
import { DiagnosisDocsAction } from "./RunDiagnosisCard";

function markup(label: string) {
return renderToStaticMarkup(
createElement(
OperatingSystemContextProvider,
{ platform: "mac" },
createElement(
ShortcutsProvider,
null,
createElement(DiagnosisDocsAction, {
action: {
kind: "docs",
label,
to: "https://example.com/help",
destinationHost: "example.com",
},
})
)
)
);
}

describe("diagnosis documentation actions", () => {
it("bounds and isolates an untrusted label without hiding the destination host", () => {
const label = `${"Read documentation ".repeat(20)}\u202eevil.test`;
const html = markup(label);
const labelStart = html.indexOf('<bdi dir="auto"');
const hostStart = html.indexOf('<bdi dir="ltr" class="shrink-0 text-text-dimmed">');

expect(html).toContain('href="https://example.com/help"');
expect(html).toContain('target="_blank"');
expect(html).toContain('rel="noopener noreferrer"');
expect(html).toContain("max-w-[24ch] truncate");
expect(html).toContain(label);
expect(labelStart).toBeGreaterThan(-1);
expect(hostStart).toBeGreaterThan(labelStart);
expect(html.slice(hostStart)).toContain("(example.com)</bdi>");
});
});
25 changes: 19 additions & 6 deletions apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { v3RunPath } from "~/utils/pathBuilder";
import { planDiagnosisActions } from "./diagnosis-actions";
import { planDiagnosisActions, type PlannedDiagnosisAction } from "./diagnosis-actions";
import { isRunFriendlyId } from "./run-id";

// No markup comes from the model, so only outbound URLs need checking.

function Em({ children }: { children: React.ReactNode }) {
return <span className="font-semibold text-text-bright">{children}</span>;
}
Expand Down Expand Up @@ -129,6 +127,23 @@ function EvidenceReference({ reference }: { reference: string }) {
return <span className="font-mono text-xs text-text-dimmed">{reference}</span>;
}

export function DiagnosisDocsAction({
action,
}: {
action: Extract<PlannedDiagnosisAction, { kind: "docs" }>;
}) {
return (
<LinkButton to={action.to} variant="docs/small" LeadingIcon={BookOpenIcon}>
<bdi dir="auto" className="block min-w-0 max-w-[24ch] truncate">
{action.label}
</bdi>
<bdi dir="ltr" className="shrink-0 text-text-dimmed">
({action.destinationHost})
</bdi>
</LinkButton>
);
}

function DiagnosisActions({ actions }: { actions: NonNullable<DiagnosisBlock["actions"]> }) {
const runPath = useRunPathResolver();
const planned = planDiagnosisActions(actions, {
Expand All @@ -141,9 +156,7 @@ function DiagnosisActions({ actions }: { actions: NonNullable<DiagnosisBlock["ac
<div className="flex flex-wrap gap-2 pt-2">
{planned.map((action, i) =>
action.kind === "docs" ? (
<LinkButton key={i} to={action.to} variant="docs/small" LeadingIcon={BookOpenIcon}>
{action.label}
</LinkButton>
<DiagnosisDocsAction key={i} action={action} />
) : (
<LinkButton key={i} to={action.to} variant="primary/small">
{action.label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ describe("planDiagnosisActions", () => {

it("drops only the unresolvable action, keeping the rest", () => {
expect(planDiagnosisActions([viewRun, docs], withoutContext)).toEqual([
{ kind: "docs", label: "Read the docs", to: docs.target },
{
kind: "docs",
label: "Read the docs",
to: docs.target,
destinationHost: "trigger.dev",
},
]);
});

Expand All @@ -37,10 +42,35 @@ describe("planDiagnosisActions", () => {
);
});

it("derives the displayed host from the resolved destination", () => {
expect(
planDiagnosisActions(
[{ ...docs, target: "https://example.com/help", label: "trigger.dev documentation" }],
resolve
)
).toEqual([
{
kind: "docs",
label: "trigger.dev documentation",
to: "https://example.com/help",
destinationHost: "example.com",
},
]);
});

it("drops a docs action with an unsafe target", () => {
expect(planDiagnosisActions([{ ...docs, target: "javascript:alert(1)" }], resolve)).toEqual([]);
});

it("drops a docs action when its resolved destination has no hostname", () => {
expect(
planDiagnosisActions([docs], {
...resolve,
docsUrl: () => "blob:https://trigger.dev/id",
})
).toEqual([]);
});

it("drops an action kind it does not know", () => {
expect(planDiagnosisActions([{ ...viewRun, kind: "replay_run" }], resolve)).toEqual([]);
});
Expand Down
26 changes: 20 additions & 6 deletions apps/webapp/app/components/dashboard-agent/diagnosis-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { isRunFriendlyId } from "./run-id";

export type DiagnosisActionInput = { kind: string; target: string; label: string };

export type PlannedDiagnosisAction = {
kind: "view_run" | "docs";
label: string;
to: string;
};
export type PlannedDiagnosisAction =
| {
kind: "view_run";
label: string;
to: string;
}
| {
kind: "docs";
label: string;
to: string;
destinationHost: string;
};

/**
* An action whose destination can't be resolved is dropped, never rendered as a
Expand All @@ -29,7 +36,14 @@ export function planDiagnosisActions(
}
if (action.kind === "docs") {
const to = resolve.docsUrl(action.target);
if (to) planned.push({ kind: "docs", label: action.label, to });
if (!to) continue;

try {
const destinationHost = new URL(to).hostname;
if (destinationHost) {
planned.push({ kind: "docs", label: action.label, to, destinationHost });
}
} catch {}
}
}

Expand Down
30 changes: 23 additions & 7 deletions apps/webapp/app/models/project.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Prisma, Project } from "@trigger.dev/database";
import type { Prisma, PrismaClientOrTransaction, Project } from "@trigger.dev/database";
import { customAlphabet, nanoid } from "nanoid";
import slug from "slug";
import { $replica, prisma } from "~/db.server";
Expand Down Expand Up @@ -146,13 +146,20 @@ export async function createProject(
return project;
}

export async function findProjectBySlug(orgSlug: string, projectSlug: string, userId: string) {
export async function findProjectBySlug(
orgSlug: string,
projectSlug: string,
userId: string,
db: PrismaClientOrTransaction = $replica
) {
// Find the project scoped to the organization, making sure the user belongs to that org
return await $replica.project.findFirst({
return await db.project.findFirst({
where: {
slug: projectSlug,
deletedAt: null,
organization: {
slug: orgSlug,
deletedAt: null,
members: { some: { userId } },
},
},
Expand All @@ -166,26 +173,35 @@ export async function findProjectBySlug(orgSlug: string, projectSlug: string, us
export async function findProjectWithOrgFlagsBySlug(
orgSlug: string,
projectSlug: string,
userId: string
userId: string,
db: PrismaClientOrTransaction = $replica
) {
return await $replica.project.findFirst({
return await db.project.findFirst({
where: {
slug: projectSlug,
deletedAt: null,
organization: {
slug: orgSlug,
deletedAt: null,
members: { some: { userId } },
},
},
include: { organization: { select: { featureFlags: true } } },
});
}

export async function findProjectByRef(externalRef: string, userId: string) {
export async function findProjectByRef(
externalRef: string,
userId: string,
db: PrismaClientOrTransaction = $replica
) {
// Find the project scoped to the organization, making sure the user belongs to that org
return await $replica.project.findFirst({
return await db.project.findFirst({
where: {
externalRef,
deletedAt: null,
organization: {
deletedAt: null,
members: { some: { userId } },
},
},
Expand Down
9 changes: 6 additions & 3 deletions apps/webapp/app/models/runtimeEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,15 @@ export async function findEnvironmentById(id: string): Promise<AuthenticatedEnvi
export async function findEnvironmentBySlug(
projectId: string,
envSlug: string,
userId: string
userId: string,
db: PrismaClientOrTransaction = $replica
): Promise<AuthenticatedEnvironment | null> {
const environment = await $replica.runtimeEnvironment.findFirst({
const environment = await db.runtimeEnvironment.findFirst({
where: {
projectId: projectId,
projectId,
slug: envSlug,
project: { deletedAt: null },
organization: { deletedAt: null },
OR: [
{
type: {
Expand Down
Loading
Loading