Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1b27cfb
Create schema and migration for organization access tokens
myftija Aug 14, 2025
0a82a56
Add helpers for creating and authenticating OATs
myftija Aug 14, 2025
c6728d0
Adapt the auth service to also accept OATs
myftija Aug 14, 2025
6d05b46
Accept OATs in the whoami v2 endpoint
myftija Aug 14, 2025
388003b
Enable deployments with the CLI using OATs
myftija Aug 14, 2025
333e14a
Avoid reading env variables directly in the token utils
myftija Aug 14, 2025
690461a
Remove duplicate cli token utils
myftija Aug 14, 2025
11ac4ab
Validate ENCRYPTION_KEY length when parsing env vars
myftija Aug 14, 2025
f4b8892
Make token utils a server-only module
myftija Aug 14, 2025
90f6858
Disallow revoking already revoked OATs
myftija Aug 14, 2025
8775603
Simplify generics in authenticateRequest
myftija Aug 14, 2025
7d092ee
Use 32 bytes mock encryption key in the test setup
myftija Aug 14, 2025
27ba07a
Update dummy encryption key values in tests and templates
myftija Aug 14, 2025
f2cee5e
Add a column in the OATs table to differentiate between user and syst…
myftija Aug 14, 2025
5477547
Simplify args for v3ProjectPath
myftija Aug 14, 2025
908c66b
Add index on org id and createdAt
myftija Aug 14, 2025
67f7e9a
Avoid storing the encrypted oat token and its obfuscated version in t…
myftija Aug 14, 2025
1267d55
Fix prisma update condition
myftija Aug 14, 2025
f253470
Add token type to the OAT table index
myftija Aug 27, 2025
f60d4b5
Accept OATs in the mcp auth flow
myftija Aug 27, 2025
d2b0045
Simplify env auth flow around the /projects endpoints
myftija Aug 27, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests-webapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
SESSION_SECRET: "secret"
MAGIC_LINK_SECRET: "secret"
ENCRYPTION_KEY: "secret"
ENCRYPTION_KEY: "dummy-encryption-keeeey-32-bytes"
DEPLOY_REGISTRY_HOST: "docker.io"
CLICKHOUSE_URL: "http://default:password@localhost:8123"

Expand Down
7 changes: 6 additions & 1 deletion apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ const EnvironmentSchema = z.object({
DATABASE_READ_REPLICA_URL: z.string().optional(),
SESSION_SECRET: z.string(),
MAGIC_LINK_SECRET: z.string(),
ENCRYPTION_KEY: z.string(),
ENCRYPTION_KEY: z
.string()
.refine(
(val) => Buffer.from(val, "utf8").length === 32,
"ENCRYPTION_KEY must be exactly 32 bytes"
),
WHITELISTED_EMAILS: z
.string()
.refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.")
Expand Down
71 changes: 17 additions & 54 deletions apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ActionFunctionArgs, json } from "@remix-run/node";
import { type ActionFunctionArgs, json } from "@remix-run/node";
import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
import { getEnvironmentFromEnv } from "./api.v1.projects.$projectRef.$env";
import {
authenticatedEnvironmentForAuthentication,
authenticateRequest,
} from "~/services/apiAuth.server";

const ParamsSchema = z.object({
projectRef: z.string(),
Expand All @@ -20,7 +21,11 @@ const RequestBodySchema = z.object({
});

export async function action({ request, params }: ActionFunctionArgs) {
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
const authenticationResult = await authenticateRequest(request, {
personalAccessToken: true,
organizationAccessToken: true,
apiKey: false,
});

if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
Expand All @@ -33,35 +38,14 @@ export async function action({ request, params }: ActionFunctionArgs) {
}

const { projectRef, env } = parsedParams.data;
const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined;

const project = await prisma.project.findFirst({
where: {
externalRef: projectRef,
organization: {
members: {
some: {
userId: authenticationResult.userId,
},
},
},
},
});

if (!project) {
return json({ error: "Project not found" }, { status: 404 });
}

const envResult = await getEnvironmentFromEnv({
projectId: project.id,
userId: authenticationResult.userId,
const runtimeEnv = await authenticatedEnvironmentForAuthentication(
authenticationResult,
projectRef,
env,
});

if (!envResult.success) {
return json({ error: envResult.error }, { status: 404 });
}

const runtimeEnv = envResult.environment;
triggerBranch
);

const parsedBody = RequestBodySchema.safeParse(await request.json());

Expand All @@ -72,29 +56,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
);
}

const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined;

let previewBranchEnvironmentId: string | undefined;

if (triggerBranch) {
const previewBranch = await prisma.runtimeEnvironment.findFirst({
where: {
projectId: project.id,
branchName: triggerBranch,
parentEnvironmentId: runtimeEnv.id,
archivedAt: null,
},
});

if (previewBranch) {
previewBranchEnvironmentId = previewBranch.id;
} else {
return json({ error: `Preview branch ${triggerBranch} not found` }, { status: 404 });
}
}

const claims = {
sub: previewBranchEnvironmentId ?? runtimeEnv.id,
sub: runtimeEnv.id,
pub: true,
...parsedBody.data.claims,
};
Expand Down
176 changes: 15 additions & 161 deletions apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { type GetProjectEnvResponse } from "@trigger.dev/core/v3";
import { type RuntimeEnvironment } from "@trigger.dev/database";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env as processEnv } from "~/env.server";
import { logger } from "~/services/logger.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
import {
authenticatedEnvironmentForAuthentication,
authenticateRequest,
} from "~/services/apiAuth.server";

const ParamsSchema = z.object({
projectRef: z.string(),
Expand All @@ -15,14 +15,6 @@ const ParamsSchema = z.object({
type ParamsSchema = z.infer<typeof ParamsSchema>;

export async function loader({ request, params }: LoaderFunctionArgs) {
logger.info("projects get env", { url: request.url });

const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
}

const parsedParams = ParamsSchema.safeParse(params);

if (!parsedParams.success) {
Expand All @@ -31,162 +23,24 @@ export async function loader({ request, params }: LoaderFunctionArgs) {

const { projectRef, env } = parsedParams.data;

const project = await prisma.project.findFirst({
where: {
externalRef: projectRef,
organization: {
members: {
some: {
userId: authenticationResult.userId,
},
},
},
},
});

if (!project) {
return json({ error: "Project not found" }, { status: 404 });
}

const envResult = await getEnvironmentFromEnv({
projectId: project.id,
userId: authenticationResult.userId,
env,
});
const authenticationResult = await authenticateRequest(request);

if (!envResult.success) {
return json({ error: envResult.error }, { status: 404 });
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}

const runtimeEnv = envResult.environment;
const environment = await authenticatedEnvironmentForAuthentication(
authenticationResult,
projectRef,
env
);

const result: GetProjectEnvResponse = {
apiKey: runtimeEnv.apiKey,
name: project.name,
apiKey: environment.apiKey,
name: environment.project.name,
apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN,
projectId: project.id,
projectId: environment.project.id,
};

return json(result);
}

export async function getEnvironmentFromEnv({
projectId,
userId,
env,
branch,
}: {
projectId: string;
userId: string;
env: ParamsSchema["env"];
branch?: string;
}): Promise<
| {
success: true;
environment: RuntimeEnvironment;
}
| {
success: false;
error: string;
}
> {
if (env === "dev") {
const environment = await prisma.runtimeEnvironment.findFirst({
where: {
projectId,
orgMember: {
userId: userId,
},
},
});

if (!environment) {
return {
success: false,
error: "Dev environment not found",
};
}

return {
success: true,
environment,
};
}

let slug: "stg" | "prod" | "preview" = "prod";
switch (env) {
case "staging":
slug = "stg";
break;
case "prod":
slug = "prod";
break;
case "preview":
slug = "preview";
break;
default:
break;
}

if (slug === "preview") {
const previewEnvironment = await prisma.runtimeEnvironment.findFirst({
where: {
projectId,
slug: "preview",
},
});

if (!previewEnvironment) {
return {
success: false,
error: "Preview environment not found",
};
}

// If no branch is provided, just return the parent preview environment
if (!branch) {
return {
success: true,
environment: previewEnvironment,
};
}

const branchEnvironment = await prisma.runtimeEnvironment.findFirst({
where: {
parentEnvironmentId: previewEnvironment.id,
branchName: branch,
},
});

if (!branchEnvironment) {
return {
success: false,
error: `Preview branch ${branch} not found`,
};
}

return {
success: true,
environment: branchEnvironment,
};
}

const environment = await prisma.runtimeEnvironment.findFirst({
where: {
projectId,
slug,
},
});

if (!environment) {
return {
success: false,
error: `${env === "staging" ? "Staging" : "Production"} environment not found`,
};
}

return {
success: true,
environment,
};
}
Loading
Loading