Skip to content

Commit c8af9d4

Browse files
committed
feat(webapp): locate runs and errors across the token organization
1 parent 4595363 commit c8af9d4

5 files changed

Lines changed: 616 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { isOrganizationMember, locateAgentObject } from "~/services/locateAgentObject.server";
4+
import { logger } from "~/services/logger.server";
5+
import { authenticateUatOrApiRequest } from "~/services/uatRoutePreamble.server";
6+
7+
/**
8+
* Deterministic org-wide locator for the Dashboard Agent. The organization comes only from the
9+
* token's claim, so there is nothing for a caller to point at another tenant.
10+
*/
11+
12+
const ParamsSchema = z.object({
13+
kind: z.enum(["run", "error"]),
14+
id: z.string().min(1),
15+
});
16+
17+
export async function loader({ request, params }: LoaderFunctionArgs) {
18+
const authentication = await authenticateUatOrApiRequest(request);
19+
const userActor = authentication?.userActor;
20+
21+
if (!userActor?.organizationId) {
22+
return json({ error: "Invalid or missing access token" }, { status: 401 });
23+
}
24+
25+
const parsedParams = ParamsSchema.safeParse(params);
26+
if (!parsedParams.success) {
27+
return json({ error: "Unknown object kind", code: "invalid_request" }, { status: 400 });
28+
}
29+
30+
const organizationId = userActor.organizationId;
31+
32+
if (!(await isOrganizationMember(organizationId, userActor.userId))) {
33+
return json(
34+
{ error: "You don't have access to that organization.", code: "forbidden_organization" },
35+
{ status: 403 }
36+
);
37+
}
38+
39+
try {
40+
return json(
41+
await locateAgentObject({
42+
kind: parsedParams.data.kind,
43+
id: parsedParams.data.id,
44+
organizationId,
45+
userId: userActor.userId,
46+
})
47+
);
48+
} catch (error) {
49+
if (error instanceof Response) throw error;
50+
logger.error("Failed to locate an object for the dashboard agent", {
51+
error,
52+
kind: parsedParams.data.kind,
53+
organizationId,
54+
});
55+
return json({ error: "Internal Server Error", code: "internal" }, { status: 500 });
56+
}
57+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Org-wide lookup for the Dashboard Agent: given a run or error id the agent could not find in
3+
* the current project/environment, name every scope inside the token's organization where it
4+
* exists. The organization is a hard boundary — an object in another org reads as not found, so
5+
* the endpoint never confirms its existence.
6+
*/
7+
8+
import { boundedIn, type RuntimeEnvironmentType } from "@trigger.dev/database";
9+
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
10+
import { $replica } from "~/db.server";
11+
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
12+
import { runStore } from "~/v3/runStore.server";
13+
14+
export type LocatedScope = {
15+
projectRef: string;
16+
projectName: string;
17+
environmentName: string;
18+
environmentType: RuntimeEnvironmentType;
19+
branchName?: string;
20+
/** Whether the caller may act in this scope. An inaccessible scope is still reported. */
21+
targetable: boolean;
22+
};
23+
24+
export type LocateResult =
25+
| { found: true; scopes: LocatedScope[]; checked: "organization" }
26+
| { found: false; checked: "organization" };
27+
28+
const NOT_FOUND: LocateResult = { found: false, checked: "organization" };
29+
30+
export type LocateTarget = {
31+
kind: "run" | "error";
32+
id: string;
33+
organizationId: string;
34+
userId: string;
35+
};
36+
37+
export async function isOrganizationMember(
38+
organizationId: string,
39+
userId: string
40+
): Promise<boolean> {
41+
const organization = await $replica.organization.findFirst({
42+
where: { id: organizationId, deletedAt: null, members: { some: { userId } } },
43+
select: { id: true },
44+
});
45+
46+
return !!organization;
47+
}
48+
49+
export async function locateAgentObject(target: LocateTarget): Promise<LocateResult> {
50+
const environmentIds =
51+
target.kind === "run"
52+
? await runEnvironmentIds(target.id)
53+
: await errorEnvironmentIds(target.id, target.organizationId);
54+
55+
const scopes = await scopesForEnvironments(environmentIds, target.organizationId, target.userId);
56+
57+
return scopes.length > 0 ? { found: true, scopes, checked: "organization" } : NOT_FOUND;
58+
}
59+
60+
/** A run friendlyId is globally unique, so this is one lookup; the org filter comes later. */
61+
async function runEnvironmentIds(friendlyId: string): Promise<string[]> {
62+
const run = await runStore.findRun({ friendlyId }, { select: { runtimeEnvironmentId: true } });
63+
64+
return run ? [run.runtimeEnvironmentId] : [];
65+
}
66+
67+
/** A fingerprint is legitimately present in many environments, so every one of them answers. */
68+
async function errorEnvironmentIds(errorId: string, organizationId: string): Promise<string[]> {
69+
let fingerprint: string;
70+
try {
71+
fingerprint = ErrorId.toId(errorId);
72+
} catch {
73+
return [];
74+
}
75+
76+
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "logs");
77+
const [queryError, rows] = await clickhouse.errors.getScopes({
78+
organizationId,
79+
errorFingerprint: fingerprint,
80+
});
81+
82+
if (queryError) {
83+
throw queryError;
84+
}
85+
86+
return (rows ?? []).map((row) => row.environment_id);
87+
}
88+
89+
async function scopesForEnvironments(
90+
environmentIds: string[],
91+
organizationId: string,
92+
userId: string
93+
): Promise<LocatedScope[]> {
94+
if (environmentIds.length === 0) {
95+
return [];
96+
}
97+
98+
const environments = await $replica.runtimeEnvironment.findMany({
99+
where: {
100+
id: { in: boundedIn(environmentIds) },
101+
organizationId,
102+
archivedAt: null,
103+
project: { deletedAt: null },
104+
},
105+
select: {
106+
slug: true,
107+
type: true,
108+
branchName: true,
109+
orgMember: { select: { userId: true } },
110+
project: { select: { externalRef: true, name: true } },
111+
},
112+
});
113+
114+
return environments.map((environment) => ({
115+
projectRef: environment.project.externalRef,
116+
projectName: environment.project.name,
117+
environmentName: environment.slug,
118+
environmentType: environment.type,
119+
...(environment.branchName ? { branchName: environment.branchName } : {}),
120+
// dev is per-user: another member's dev environment exists but can't be acted in.
121+
targetable: environment.type !== "DEVELOPMENT" || environment.orgMember?.userId === userId,
122+
}));
123+
}

0 commit comments

Comments
 (0)