From 8f83bcc20a43234ca36e34a6112990dd5eb69a28 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 29 Aug 2025 16:04:41 +0200 Subject: [PATCH 01/15] Add schemas for gh app installations --- .../migration.sql | 102 ++++++++++++++++++ .../prisma/migrations/migration_lock.toml | 2 +- .../database/prisma/schema.prisma | 50 +++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql diff --git a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql new file mode 100644 index 0000000000..0be09fcb1c --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql @@ -0,0 +1,102 @@ +-- CreateEnum +CREATE TYPE "public"."GithubRepositorySelection" AS ENUM ('ALL', 'SELECTED'); + +-- DropIndex +DROP INDEX "public"."SecretStore_key_idx"; + +-- DropIndex +DROP INDEX "public"."TaskRun_runtimeEnvironmentId_createdAt_idx"; + +-- DropIndex +DROP INDEX "public"."TaskRun_runtimeEnvironmentId_id_idx"; + +-- AlterTable +ALTER TABLE "public"."_BackgroundWorkerToBackgroundWorkerFile" ADD CONSTRAINT "_BackgroundWorkerToBackgroundWorkerFile_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "public"."_BackgroundWorkerToBackgroundWorkerFile_AB_unique"; + +-- AlterTable +ALTER TABLE "public"."_BackgroundWorkerToTaskQueue" ADD CONSTRAINT "_BackgroundWorkerToTaskQueue_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "public"."_BackgroundWorkerToTaskQueue_AB_unique"; + +-- AlterTable +ALTER TABLE "public"."_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "public"."_TaskRunToTaskRunTag_AB_unique"; + +-- AlterTable +ALTER TABLE "public"."_WaitpointRunConnections" ADD CONSTRAINT "_WaitpointRunConnections_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "public"."_WaitpointRunConnections_AB_unique"; + +-- AlterTable +ALTER TABLE "public"."_completedWaitpoints" ADD CONSTRAINT "_completedWaitpoints_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "public"."_completedWaitpoints_AB_unique"; + +-- CreateTable +CREATE TABLE "public"."GithubAppInstallation" ( + "id" TEXT NOT NULL, + "appInstallationId" INTEGER NOT NULL, + "targetId" INTEGER NOT NULL, + "targetType" TEXT NOT NULL, + "permissions" JSONB, + "repositorySelection" "public"."GithubRepositorySelection" NOT NULL, + "installedBy" TEXT, + "organizationId" TEXT NOT NULL, + "deletedAt" TIMESTAMP(3), + "suspendedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GithubAppInstallation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."GithubRepository" ( + "id" TEXT NOT NULL, + "githubId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "htmlUrl" TEXT NOT NULL, + "private" BOOLEAN NOT NULL, + "removedAt" TIMESTAMP(3), + "installationId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GithubRepository_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GithubAppInstallation_appInstallationId_key" ON "public"."GithubAppInstallation"("appInstallationId"); + +-- CreateIndex +CREATE INDEX "GithubAppInstallation_organizationId_idx" ON "public"."GithubAppInstallation"("organizationId"); + +-- CreateIndex +CREATE INDEX "GithubRepository_installationId_idx" ON "public"."GithubRepository"("installationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "GithubRepository_installationId_githubId_key" ON "public"."GithubRepository"("installationId", "githubId"); + +-- CreateIndex +CREATE INDEX "SecretStore_key_idx" ON "public"."SecretStore"("key" text_pattern_ops); + +-- CreateIndex +CREATE INDEX "TaskRun_runtimeEnvironmentId_id_idx" ON "public"."TaskRun"("runtimeEnvironmentId", "id" DESC); + +-- CreateIndex +CREATE INDEX "TaskRun_runtimeEnvironmentId_createdAt_idx" ON "public"."TaskRun"("runtimeEnvironmentId", "createdAt" DESC); + +-- AddForeignKey +ALTER TABLE "public"."GithubAppInstallation" ADD CONSTRAINT "GithubAppInstallation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."GithubRepository" ADD CONSTRAINT "GithubRepository_installationId_fkey" FOREIGN KEY ("installationId") REFERENCES "public"."GithubAppInstallation"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/internal-packages/database/prisma/migrations/migration_lock.toml b/internal-packages/database/prisma/migrations/migration_lock.toml index 99e4f20090..044d57cdb0 100644 --- a/internal-packages/database/prisma/migrations/migration_lock.toml +++ b/internal-packages/database/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) +# It should be added in your version-control system (e.g., Git) provider = "postgresql" diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index be10d47f71..9acd6d4264 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -210,6 +210,7 @@ model Organization { workerGroups WorkerInstanceGroup[] workerInstances WorkerInstance[] executionSnapshots TaskRunExecutionSnapshot[] + githubAppInstallations GithubAppInstallation[] } model OrgMember { @@ -2238,3 +2239,52 @@ model TaskEventPartitioned { // Used for getting all logs for a run @@index([runId]) } + +enum GithubRepositorySelection { + ALL + SELECTED +} + +model GithubAppInstallation { + id String @id @default(cuid()) + + appInstallationId Int @unique + targetId Int + targetType String + permissions Json? + repositorySelection GithubRepositorySelection + installedBy String? + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId String + + repositories GithubRepository[] + + deletedAt DateTime? + suspendedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([organizationId]) +} + +model GithubRepository { + id String @id @default(cuid()) + + githubId Int + name String + fullName String + htmlUrl String + private Boolean + + removedAt DateTime? + + installation GithubAppInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + installationId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([installationId, githubId]) + @@index([installationId]) +} From 4fb34c771ca06497347131c7fbd8e3e0f676a352 Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 29 Aug 2025 16:06:38 +0200 Subject: [PATCH 02/15] Implement gh app installation flow --- apps/webapp/app/env.server.ts | 6 + .../app/routes/_app.github.callback/route.tsx | 74 ++++ .../app/routes/_app.github.install/route.tsx | 49 +++ apps/webapp/app/services/gitHub.server.ts | 73 ++++ .../app/services/gitHubSession.server.ts | 120 ++++++ apps/webapp/package.json | 1 + pnpm-lock.yaml | 398 +++++++++++++++++- 7 files changed, 713 insertions(+), 8 deletions(-) create mode 100644 apps/webapp/app/routes/_app.github.callback/route.tsx create mode 100644 apps/webapp/app/routes/_app.github.install/route.tsx create mode 100644 apps/webapp/app/services/gitHub.server.ts create mode 100644 apps/webapp/app/services/gitHubSession.server.ts diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index f20c02f6a8..bc5c7a115c 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -59,6 +59,12 @@ const EnvironmentSchema = z.object({ SMTP_USER: z.string().optional(), SMTP_PASSWORD: z.string().optional(), + // GitHub App + GITHUB_APP_ID: z.string(), + GITHUB_APP_PRIVATE_KEY: z.string(), + GITHUB_APP_WEBHOOK_SECRET: z.string(), + GITHUB_APP_SLUG: z.string(), + PLAIN_API_KEY: z.string().optional(), WORKER_SCHEMA: z.string().default("graphile_worker"), WORKER_CONCURRENCY: z.coerce.number().int().default(10), diff --git a/apps/webapp/app/routes/_app.github.callback/route.tsx b/apps/webapp/app/routes/_app.github.callback/route.tsx new file mode 100644 index 0000000000..6023afbe75 --- /dev/null +++ b/apps/webapp/app/routes/_app.github.callback/route.tsx @@ -0,0 +1,74 @@ +import { type LoaderFunctionArgs } from "@remix-run/node"; +import { z } from "zod"; +import { validateGitHubAppInstallSession } from "~/services/gitHubSession.server"; +import { linkGitHubAppInstallation } from "~/services/gitHub.server"; +import { logger } from "~/services/logger.server"; +import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; +import { tryCatch } from "@trigger.dev/core"; + +const QuerySchema = z.object({ + installation_id: z.coerce.number(), + setup_action: z.enum(["install", "update", "request"]), + state: z.string(), +}); + +export async function loader({ request }: LoaderFunctionArgs) { + const url = new URL(request.url); + const queryParams = Object.fromEntries(url.searchParams); + const cookieHeader = request.headers.get("Cookie"); + + const result = QuerySchema.safeParse(queryParams); + + if (!result.success) { + logger.warn("GitHub App callback with invalid params", { + queryParams, + }); + return redirectWithErrorMessage("/", request, "Failed to install GitHub App"); + } + + const { installation_id, setup_action, state } = result.data; + + const sessionResult = await validateGitHubAppInstallSession(cookieHeader, state); + + if (!sessionResult.valid) { + logger.error("GitHub App callback with invalid session", { + state, + installation_id, + error: sessionResult.error, + }); + + return redirectWithErrorMessage("/", request, "Failed to install GitHub App"); + } + + const { organizationId, redirectTo } = sessionResult; + + switch (setup_action) { + case "install": + case "update": { + const [error] = await tryCatch(linkGitHubAppInstallation(installation_id, organizationId)); + + if (error) { + logger.error("Failed to link GitHub App installation", { + error, + }); + return redirectWithErrorMessage(redirectTo, request, "Failed to install GitHub App"); + } + + return redirectWithSuccessMessage(redirectTo, request, "GitHub App installed successfully"); + } + + case "request": { + // This happens when a non-admin user requests installation + // The installation_id won't be available until an admin approves + logger.info("GitHub App installation requested, awaiting approval", { + state, + }); + + return redirectWithSuccessMessage(redirectTo, request, "GitHub App installation requested"); + } + + default: + setup_action satisfies never; + return redirectWithErrorMessage(redirectTo, request, "Failed to install GitHub App"); + } +} diff --git a/apps/webapp/app/routes/_app.github.install/route.tsx b/apps/webapp/app/routes/_app.github.install/route.tsx new file mode 100644 index 0000000000..b47fbbbd32 --- /dev/null +++ b/apps/webapp/app/routes/_app.github.install/route.tsx @@ -0,0 +1,49 @@ +import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { redirect } from "remix-typedjson"; +import { z } from "zod"; +import { $replica } from "~/db.server"; +import { createGitHubAppInstallSession } from "~/services/gitHubSession.server"; +import { requireUser } from "~/services/session.server"; +import { newOrganizationPath } from "~/utils/pathBuilder"; +import { logger } from "~/services/logger.server"; + +const QuerySchema = z.object({ + org_slug: z.string(), + redirect_to: z.string(), +}); + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const searchParams = new URL(request.url).searchParams; + const parsed = QuerySchema.safeParse(Object.fromEntries(searchParams)); + + if (!parsed.success) { + logger.warn("GitHub App installation redirect with invalid params", { + searchParams, + error: parsed.error, + }); + throw redirect("/"); + } + + const { org_slug, redirect_to } = parsed.data; + const user = await requireUser(request); + + const org = await $replica.organization.findFirst({ + where: { slug: org_slug, members: { some: { userId: user.id } }, deletedAt: null }, + orderBy: { createdAt: "desc" }, + select: { + id: true, + }, + }); + + if (!org) { + throw redirect(newOrganizationPath()); + } + + const { url, cookieHeader } = await createGitHubAppInstallSession(org.id, redirect_to); + + return redirect(url, { + headers: { + "Set-Cookie": cookieHeader, + }, + }); +}; diff --git a/apps/webapp/app/services/gitHub.server.ts b/apps/webapp/app/services/gitHub.server.ts new file mode 100644 index 0000000000..4224e4e247 --- /dev/null +++ b/apps/webapp/app/services/gitHub.server.ts @@ -0,0 +1,73 @@ +import { App, type Octokit } from "octokit"; +import { env } from "../env.server"; +import { prisma } from "~/db.server"; + +export const githubApp = new App({ + appId: env.GITHUB_APP_ID, + privateKey: env.GITHUB_APP_PRIVATE_KEY, + webhooks: { + secret: env.GITHUB_APP_WEBHOOK_SECRET, + }, +}); + +/** + * Links a GitHub App installation to a Trigger organization + */ +export async function linkGitHubAppInstallation( + installationId: number, + organizationId: string +): Promise { + const octokit = await githubApp.getInstallationOctokit(installationId); + const { data: installation } = await octokit.rest.apps.getInstallation({ + installation_id: installationId, + }); + + const repositories = await fetchInstallationRepositories(octokit, installationId); + + const repositorySelection = installation.repository_selection === "all" ? "ALL" : "SELECTED"; + + await prisma.githubAppInstallation.create({ + data: { + appInstallationId: installationId, + organizationId, + targetId: installation.target_id, + targetType: installation.target_type, + permissions: installation.permissions, + repositorySelection, + repositories: { + create: repositories, + }, + }, + }); +} + +async function fetchInstallationRepositories(octokit: Octokit, installationId: number) { + const all = []; + let page = 1; + const perPage = 100; + const maxPages = 3; + + while (page <= maxPages) { + const { data: repoData } = await octokit.rest.apps.listReposAccessibleToInstallation({ + installation_id: installationId, + per_page: perPage, + page, + }); + + all.push(...repoData.repositories); + + if (repoData.repositories.length < perPage) { + break; + } + + page++; + } + + return all.map((repo) => ({ + githubId: repo.id, + name: repo.name, + fullName: repo.full_name, + htmlUrl: repo.html_url, + private: repo.private, + })); +} diff --git a/apps/webapp/app/services/gitHubSession.server.ts b/apps/webapp/app/services/gitHubSession.server.ts new file mode 100644 index 0000000000..531662529b --- /dev/null +++ b/apps/webapp/app/services/gitHubSession.server.ts @@ -0,0 +1,120 @@ +import { createCookieSessionStorage } from "@remix-run/node"; +import { randomBytes } from "crypto"; +import { env } from "../env.server"; +import { logger } from "./logger.server"; + +const sessionStorage = createCookieSessionStorage({ + cookie: { + name: "__github_app_install", + httpOnly: true, + maxAge: 60 * 60, // 1 hour + path: "/", + sameSite: "lax", + secrets: [env.SESSION_SECRET], + secure: env.NODE_ENV === "production", + }, +}); + +/** + * Creates a secure session for GitHub App installation with organization tracking + */ +export async function createGitHubAppInstallSession( + organizationId: string, + redirectTo: string +): Promise<{ url: string; cookieHeader: string }> { + const state = randomBytes(32).toString("hex"); + + const session = await sessionStorage.getSession(); + session.set("organizationId", organizationId); + session.set("redirectTo", redirectTo); + session.set("state", state); + session.set("createdAt", Date.now()); + + const githubAppSlug = env.GITHUB_APP_SLUG; + + // the state query param gets passed through to the installation callback + const url = `https://github.com/apps/${githubAppSlug}/installations/new?state=${state}`; + + const cookieHeader = await sessionStorage.commitSession(session); + + return { url, cookieHeader }; +} + +/** + * Validates and retrieves the GitHub App installation session + */ +export async function validateGitHubAppInstallSession( + cookieHeader: string | null, + state: string +): Promise< + { valid: true; organizationId: string; redirectTo: string } | { valid: false; error?: string } +> { + if (!cookieHeader) { + return { + valid: false, + error: "No installation session cookie found", + }; + } + + const session = await sessionStorage.getSession(cookieHeader); + + const sessionState = session.get("state"); + const organizationId = session.get("organizationId"); + const redirectTo = session.get("redirectTo"); + const createdAt = session.get("createdAt"); + + if (!sessionState || !organizationId || !createdAt || !redirectTo) { + logger.warn("GitHub App installation session missing required fields", { + hasState: !!sessionState, + hasOrgId: !!organizationId, + hasCreatedAt: !!createdAt, + hasRedirectTo: !!redirectTo, + }); + + return { + valid: false, + error: "invalid_session_data", + }; + } + + if (sessionState !== state) { + logger.warn("GitHub App installation state mismatch", { + expectedState: sessionState, + receivedState: state, + }); + return { + valid: false, + error: "state_mismatch", + }; + } + + const expirationTime = createdAt + 60 * 60 * 1000; + if (Date.now() > expirationTime) { + logger.warn("GitHub App installation session expired", { + createdAt: new Date(createdAt), + now: new Date(), + }); + return { + valid: false, + error: "session_expired", + }; + } + + return { + valid: true, + organizationId, + redirectTo, + }; +} + +/** + * Destroys the GitHub App installation cookie session + */ +export async function destroyGitHubAppInstallSession(cookieHeader: string | null): Promise { + if (!cookieHeader) { + return ""; + } + + const session = await sessionStorage.getSession(cookieHeader); + return await sessionStorage.destroySession(session); +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index a09bcc5a66..a029494240 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -160,6 +160,7 @@ "morgan": "^1.10.0", "nanoid": "3.3.8", "non.geist": "^1.0.2", + "octokit": "^3.2.1", "ohash": "^1.1.3", "openai": "^4.33.1", "p-limit": "^6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4ddc72986..a2c6c79547 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -587,6 +587,9 @@ importers: non.geist: specifier: ^1.0.2 version: 1.0.2 + octokit: + specifier: ^3.2.1 + version: 3.2.2 ohash: specifier: ^1.1.3 version: 1.1.3 @@ -8546,6 +8549,262 @@ packages: which: 3.0.1 dev: true + /@octokit/app@14.1.0: + resolution: {integrity: sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-app': 6.1.4 + '@octokit/auth-unauthenticated': 5.0.1 + '@octokit/core': 5.2.2 + '@octokit/oauth-app': 6.1.0 + '@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.2) + '@octokit/types': 12.6.0 + '@octokit/webhooks': 12.3.2 + dev: false + + /@octokit/auth-app@6.1.4: + resolution: {integrity: sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.1.0 + '@octokit/auth-oauth-user': 4.1.0 + '@octokit/request': 8.4.1 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + deprecation: 2.3.1 + lru-cache: /@wolfy1339/lru-cache@11.0.2-patch.1 + universal-github-app-jwt: 1.2.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-app@7.1.0: + resolution: {integrity: sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.1.0 + '@octokit/auth-oauth-user': 4.1.0 + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 + '@types/btoa-lite': 1.0.2 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-device@6.1.0: + resolution: {integrity: sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-methods': 4.1.0 + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-user@4.1.0: + resolution: {integrity: sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.1.0 + '@octokit/oauth-methods': 4.1.0 + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-token@4.0.0: + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/auth-unauthenticated@5.0.1: + resolution: {integrity: sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.1.1 + '@octokit/types': 12.6.0 + dev: false + + /@octokit/core@5.2.2: + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.1 + '@octokit/request': 8.4.1 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/endpoint@9.0.6: + resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/graphql@7.1.1: + resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/oauth-app@6.1.0: + resolution: {integrity: sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.1.0 + '@octokit/auth-oauth-user': 4.1.0 + '@octokit/auth-unauthenticated': 5.0.1 + '@octokit/core': 5.2.2 + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/oauth-methods': 4.1.0 + '@types/aws-lambda': 8.10.152 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/oauth-authorization-url@6.0.2: + resolution: {integrity: sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/oauth-methods@4.1.0: + resolution: {integrity: sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/request': 8.4.1 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + btoa-lite: 1.0.0 + dev: false + + /@octokit/openapi-types@20.0.0: + resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} + dev: false + + /@octokit/openapi-types@24.2.0: + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + dev: false + + /@octokit/plugin-paginate-graphql@4.0.1(@octokit/core@5.2.2): + resolution: {integrity: sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.2.2 + dev: false + + /@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2): + resolution: {integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 + dev: false + + /@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2): + resolution: {integrity: sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.2 + '@octokit/types': 12.6.0 + dev: false + + /@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2): + resolution: {integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5 + dependencies: + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 + dev: false + + /@octokit/plugin-retry@6.1.0(@octokit/core@5.2.2): + resolution: {integrity: sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.2 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/plugin-throttling@8.2.0(@octokit/core@5.2.2): + resolution: {integrity: sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5.0.0 + dependencies: + '@octokit/core': 5.2.2 + '@octokit/types': 12.6.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/request-error@5.1.1: + resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 13.10.0 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request@8.4.1: + resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/endpoint': 9.0.6 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/types@12.6.0: + resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} + dependencies: + '@octokit/openapi-types': 20.0.0 + dev: false + + /@octokit/types@13.10.0: + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + dependencies: + '@octokit/openapi-types': 24.2.0 + dev: false + + /@octokit/webhooks-methods@4.1.0: + resolution: {integrity: sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==} + engines: {node: '>= 18'} + dev: false + + /@octokit/webhooks-types@7.6.1: + resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==} + dev: false + + /@octokit/webhooks@12.3.2: + resolution: {integrity: sha512-exj1MzVXoP7xnAcAB3jZ97pTvVPkQF9y6GA/dvYC47HV7vLv+24XRS6b/v/XnyikpEuvMhugEXdGtAlU086WkQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.1.1 + '@octokit/webhooks-methods': 4.1.0 + '@octokit/webhooks-types': 7.6.1 + aggregate-error: 3.1.0 + dev: false + /@one-ini/wasm@0.1.1: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} dev: false @@ -17538,6 +17797,10 @@ packages: resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} dev: true + /@types/aws-lambda@8.10.152: + resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} + dev: false + /@types/bcryptjs@2.4.2: resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==} dev: true @@ -17549,6 +17812,10 @@ packages: '@types/node': 20.14.14 dev: true + /@types/btoa-lite@1.0.2: + resolution: {integrity: sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==} + dev: false + /@types/bun@1.1.6: resolution: {integrity: sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA==} dependencies: @@ -17808,6 +18075,13 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/jsonwebtoken@9.0.10: + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + dependencies: + '@types/ms': 0.7.31 + '@types/node': 20.14.14 + dev: false + /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: @@ -17926,7 +18200,6 @@ packages: resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} dependencies: undici-types: 6.20.0 - dev: false /@types/nodemailer@6.4.17: resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} @@ -19012,6 +19285,11 @@ packages: xstate: 5.18.1 dev: false + /@wolfy1339/lru-cache@11.0.2-patch.1: + resolution: {integrity: sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==} + engines: {node: 18 >=18.20 || 20 || >=22} + dev: false + /@xobotyi/scrollbar-width@1.9.5: resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} dev: false @@ -19181,7 +19459,6 @@ packages: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - dev: true /aggregate-error@4.0.1: resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} @@ -19878,6 +20155,10 @@ packages: dependencies: tweetnacl: 0.14.5 + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + dev: false + /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -19958,6 +20239,10 @@ packages: - supports-color dev: false + /bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + dev: false + /bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} dev: false @@ -20023,6 +20308,10 @@ packages: update-browserslist-db: 1.1.3(browserslist@4.25.0) dev: true + /btoa-lite@1.0.0: + resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==} + dev: false + /buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: false @@ -20032,6 +20321,10 @@ packages: engines: {node: '>=8.0.0'} dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -20453,7 +20746,6 @@ packages: /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - dev: true /clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} @@ -21440,6 +21732,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -21702,6 +21998,12 @@ packages: safer-buffer: 2.1.2 dev: false + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /editorconfig@1.0.4: resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} engines: {node: '>=14'} @@ -21834,7 +22136,7 @@ packages: engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.2.2 /enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -25124,7 +25426,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.14.14 + '@types/node': 22.13.9 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -25338,6 +25640,22 @@ packages: engines: {node: '>=0.10.0'} dev: false + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + dev: false + /jsprim@1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} @@ -25361,6 +25679,21 @@ packages: engines: {node: '>=12.20'} dev: true + /jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + dev: false + /keyv@3.1.0: resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} dependencies: @@ -25742,21 +26075,40 @@ packages: resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} dev: false + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + /lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} dev: false + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + /lodash.isfunction@3.0.9: resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} dev: false + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + /lodash.isnil@4.0.0: resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} dev: false + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - dev: true + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false /lodash.isundefined@3.0.1: resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} @@ -25769,6 +26121,10 @@ packages: resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} dev: false + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true @@ -27631,6 +27987,23 @@ packages: /obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + /octokit@3.2.2: + resolution: {integrity: sha512-7Abo3nADdja8l/aglU6Y3lpnHSfv0tw7gFPiqzry/yCU+2gTAX7R1roJ8hJrxIK+S1j+7iqRJXtmuHJ/UDsBhQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/app': 14.1.0 + '@octokit/core': 5.2.2 + '@octokit/oauth-app': 6.1.0 + '@octokit/plugin-paginate-graphql': 4.0.1(@octokit/core@5.2.2) + '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.2) + '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.2) + '@octokit/plugin-retry': 6.1.0(@octokit/core@5.2.2) + '@octokit/plugin-throttling': 8.2.0(@octokit/core@5.2.2) + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + '@octokit/webhooks': 12.3.2 + dev: false + /ohash@1.1.3: resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} dev: false @@ -32014,7 +32387,6 @@ packages: /tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} - dev: true /tar-fs@2.1.3: resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} @@ -33000,7 +33372,6 @@ packages: /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - dev: false /undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} @@ -33152,6 +33523,17 @@ packages: cookie: 0.6.0 dev: false + /universal-github-app-jwt@1.2.0: + resolution: {integrity: sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==} + dependencies: + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.2 + dev: false + + /universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + dev: false + /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} From 1900e6cca30a08e13b8c3f8b16893a0f8c099fad Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 29 Aug 2025 16:17:59 +0200 Subject: [PATCH 03/15] Make the gh app configs optional --- apps/webapp/app/env.server.ts | 2265 +++++++++-------- apps/webapp/app/routes/auth.github.ts | 4 +- apps/webapp/app/services/gitHub.server.ts | 21 +- .../app/services/gitHubSession.server.ts | 4 + 4 files changed, 1184 insertions(+), 1110 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index bc5c7a115c..e30022526b 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -3,1106 +3,1171 @@ import { BoolEnv } from "./utils/boolEnv"; import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; -const EnvironmentSchema = z.object({ - NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]), - DATABASE_URL: z - .string() - .refine( - isValidDatabaseUrl, - "DATABASE_URL is invalid, for details please check the additional output above this message." - ), - DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10), - DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60), - DATABASE_CONNECTION_TIMEOUT: z.coerce.number().int().default(20), - DIRECT_URL: z - .string() - .refine( - isValidDatabaseUrl, - "DIRECT_URL is invalid, for details please check the additional output above this message." - ), - DATABASE_READ_REPLICA_URL: z.string().optional(), - SESSION_SECRET: z.string(), - MAGIC_LINK_SECRET: 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.") - .optional(), - ADMIN_EMAILS: z.string().refine(isValidRegex, "ADMIN_EMAILS must be a valid regex.").optional(), - REMIX_APP_PORT: z.string().optional(), - LOGIN_ORIGIN: z.string().default("http://localhost:3030"), - APP_ORIGIN: z.string().default("http://localhost:3030"), - API_ORIGIN: z.string().optional(), - STREAM_ORIGIN: z.string().optional(), - ELECTRIC_ORIGIN: z.string().default("http://localhost:3060"), - // A comma separated list of electric origins to shard into different electric instances by environmentId - // example: "http://localhost:3060,http://localhost:3061,http://localhost:3062" - ELECTRIC_ORIGIN_SHARDS: z.string().optional(), - APP_ENV: z.string().default(process.env.NODE_ENV), - SERVICE_NAME: z.string().default("trigger.dev webapp"), - POSTHOG_PROJECT_KEY: z.string().default("phc_LFH7kJiGhdIlnO22hTAKgHpaKhpM8gkzWAFvHmf5vfS"), - TRIGGER_TELEMETRY_DISABLED: z.string().optional(), - AUTH_GITHUB_CLIENT_ID: z.string().optional(), - AUTH_GITHUB_CLIENT_SECRET: z.string().optional(), - EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), - FROM_EMAIL: z.string().optional(), - REPLY_TO_EMAIL: z.string().optional(), - RESEND_API_KEY: z.string().optional(), - SMTP_HOST: z.string().optional(), - SMTP_PORT: z.coerce.number().optional(), - SMTP_SECURE: BoolEnv.optional(), - SMTP_USER: z.string().optional(), - SMTP_PASSWORD: z.string().optional(), - - // GitHub App - GITHUB_APP_ID: z.string(), - GITHUB_APP_PRIVATE_KEY: z.string(), - GITHUB_APP_WEBHOOK_SECRET: z.string(), - GITHUB_APP_SLUG: z.string(), - - PLAIN_API_KEY: z.string().optional(), - WORKER_SCHEMA: z.string().default("graphile_worker"), - WORKER_CONCURRENCY: z.coerce.number().int().default(10), - WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - WORKER_ENABLED: z.string().default("true"), - GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000), - DISABLE_SSE: z.string().optional(), - OPENAI_API_KEY: z.string().optional(), - - // Redis options - REDIS_HOST: z.string().optional(), - REDIS_READER_HOST: z.string().optional(), - REDIS_READER_PORT: z.coerce.number().optional(), - REDIS_PORT: z.coerce.number().optional(), - REDIS_USERNAME: z.string().optional(), - REDIS_PASSWORD: z.string().optional(), - REDIS_TLS_DISABLED: z.string().optional(), - - RATE_LIMIT_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - RATE_LIMIT_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - RATE_LIMIT_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - RATE_LIMIT_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - RATE_LIMIT_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - RATE_LIMIT_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - RATE_LIMIT_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - CACHE_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - CACHE_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - CACHE_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - CACHE_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - CACHE_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - CACHE_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - CACHE_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - CACHE_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - REALTIME_STREAMS_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - REALTIME_STREAMS_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - REALTIME_STREAMS_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - REALTIME_STREAMS_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - REALTIME_STREAMS_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - REALTIME_STREAMS_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - REALTIME_STREAMS_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - REALTIME_STREAMS_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - REALTIME_MAXIMUM_CREATED_AT_FILTER_AGE_IN_MS: z.coerce - .number() - .int() - .default(24 * 60 * 60 * 1000), // 1 day in milliseconds - - PUBSUB_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - PUBSUB_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - PUBSUB_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - PUBSUB_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - PUBSUB_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - PUBSUB_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - PUBSUB_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(100), - DEFAULT_ENV_EXECUTION_CONCURRENCY_BURST_FACTOR: z.coerce.number().default(1.0), - DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(300), - DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1), - - //API Rate limiting - /** - * @example "60s" - * @example "1m" - * @example "1h" - * @example "1d" - * @example "1000ms" - * @example "1000s" - */ - API_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds - API_RATE_LIMIT_MAX: z.coerce.number().int().default(750), // allow bursts of 750 requests - API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refix 250 tokens every 10 seconds - API_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"), - API_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"), - API_RATE_LIMIT_LIMITER_LOGS_ENABLED: z.string().default("0"), - - API_RATE_LIMIT_JWT_WINDOW: z.string().default("1m"), - API_RATE_LIMIT_JWT_TOKENS: z.coerce.number().int().default(60), - - //v3 - PROVIDER_SECRET: z.string().default("provider-secret"), - COORDINATOR_SECRET: z.string().default("coordinator-secret"), - DEPOT_TOKEN: z.string().optional(), - DEPOT_ORG_ID: z.string().optional(), - DEPOT_REGION: z.string().default("us-east-1"), - - // Deployment registry (v3) - DEPLOY_REGISTRY_HOST: z.string().min(1), - DEPLOY_REGISTRY_USERNAME: z.string().optional(), - DEPLOY_REGISTRY_PASSWORD: z.string().optional(), - DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"), - DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2" - DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(), - DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(), - - // Deployment registry (v4) - falls back to v3 registry if not specified - V4_DEPLOY_REGISTRY_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_HOST) - .pipe(z.string().min(1)), // Ensure final type is required string - V4_DEPLOY_REGISTRY_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_USERNAME), - V4_DEPLOY_REGISTRY_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_PASSWORD), - V4_DEPLOY_REGISTRY_NAMESPACE: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_NAMESPACE) - .pipe(z.string().min(1).default("trigger")), // Ensure final type is required string - V4_DEPLOY_REGISTRY_ECR_TAGS: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_TAGS), - V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN), - V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z - .string() - .optional() - .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID), - - DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"), - DEPLOY_TIMEOUT_MS: z.coerce - .number() - .int() - .default(60 * 1000 * 8), // 8 minutes - - OBJECT_STORE_BASE_URL: z.string().optional(), - OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(), - OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(), - OBJECT_STORE_REGION: z.string().optional(), - OBJECT_STORE_SERVICE: z.string().default("s3"), - EVENTS_BATCH_SIZE: z.coerce.number().int().default(100), - EVENTS_BATCH_INTERVAL: z.coerce.number().int().default(1000), - EVENTS_DEFAULT_LOG_RETENTION: z.coerce.number().int().default(7), - EVENTS_MIN_CONCURRENCY: z.coerce.number().int().default(1), - EVENTS_MAX_CONCURRENCY: z.coerce.number().int().default(10), - EVENTS_MAX_BATCH_SIZE: z.coerce.number().int().default(500), - EVENTS_MEMORY_PRESSURE_THRESHOLD: z.coerce.number().int().default(5000), - EVENTS_LOAD_SHEDDING_THRESHOLD: z.coerce.number().int().default(100000), - EVENTS_LOAD_SHEDDING_ENABLED: z.string().default("1"), - SHARED_QUEUE_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10), - SHARED_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(100), - SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS: z.coerce.number().int().default(100), - SHARED_QUEUE_CONSUMER_EMIT_RESUME_DEPENDENCY_TIMEOUT_MS: z.coerce.number().int().default(1000), - SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE: z.coerce.number().int().default(25), - - MANAGED_WORKER_SECRET: z.string().default("managed-secret"), - - // Development OTEL environment variables - DEV_OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(), - // If this is set to 1, then the below variables are used to configure the batch processor for spans and logs - DEV_OTEL_BATCH_PROCESSING_ENABLED: z.string().default("0"), - DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), - DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS: z.string().default("200"), - DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), - DEV_OTEL_SPAN_MAX_QUEUE_SIZE: z.string().default("512"), - DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), - DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS: z.string().default("200"), - DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), - DEV_OTEL_LOG_MAX_QUEUE_SIZE: z.string().default("512"), - - PROD_OTEL_BATCH_PROCESSING_ENABLED: z.string().default("0"), - PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), - PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS: z.string().default("200"), - PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), - PROD_OTEL_SPAN_MAX_QUEUE_SIZE: z.string().default("512"), - PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), - PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS: z.string().default("200"), - PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), - PROD_OTEL_LOG_MAX_QUEUE_SIZE: z.string().default("512"), - - TRIGGER_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: z.string().default("1024"), - TRIGGER_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT: z.string().default("1024"), - TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: z.string().default("131072"), - TRIGGER_OTEL_LOG_ATTRIBUTE_VALUE_LENGTH_LIMIT: z.string().default("131072"), - TRIGGER_OTEL_SPAN_EVENT_COUNT_LIMIT: z.string().default("10"), - TRIGGER_OTEL_LINK_COUNT_LIMIT: z.string().default("2"), - TRIGGER_OTEL_ATTRIBUTE_PER_LINK_COUNT_LIMIT: z.string().default("10"), - TRIGGER_OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT: z.string().default("10"), - - CHECKPOINT_THRESHOLD_IN_MS: z.coerce.number().int().default(30000), - - // Internal OTEL environment variables - INTERNAL_OTEL_TRACE_EXPORTER_URL: z.string().optional(), - INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADERS: z.string().optional(), - INTERNAL_OTEL_TRACE_LOGGING_ENABLED: z.string().default("1"), - // this means 1/20 traces or 5% of traces will be sampled (sampled = recorded) - INTERNAL_OTEL_TRACE_SAMPLING_RATE: z.string().default("20"), - INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED: z.string().default("0"), - INTERNAL_OTEL_TRACE_DISABLED: z.string().default("0"), - - INTERNAL_OTEL_LOG_EXPORTER_URL: z.string().optional(), - INTERNAL_OTEL_METRIC_EXPORTER_URL: z.string().optional(), - INTERNAL_OTEL_METRIC_EXPORTER_AUTH_HEADERS: z.string().optional(), - INTERNAL_OTEL_METRIC_EXPORTER_ENABLED: z.string().default("0"), - INTERNAL_OTEL_METRIC_EXPORTER_INTERVAL_MS: z.coerce.number().int().default(30_000), - INTERNAL_OTEL_HOST_METRICS_ENABLED: BoolEnv.default(true), - INTERNAL_OTEL_NODEJS_METRICS_ENABLED: BoolEnv.default(true), - INTERNAL_OTEL_ADDITIONAL_DETECTORS_ENABLED: BoolEnv.default(true), - - ORG_SLACK_INTEGRATION_CLIENT_ID: z.string().optional(), - ORG_SLACK_INTEGRATION_CLIENT_SECRET: z.string().optional(), - - /** These enable the alerts feature in v3 */ - ALERT_EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), - ALERT_FROM_EMAIL: z.string().optional(), - ALERT_REPLY_TO_EMAIL: z.string().optional(), - ALERT_RESEND_API_KEY: z.string().optional(), - ALERT_SMTP_HOST: z.string().optional(), - ALERT_SMTP_PORT: z.coerce.number().optional(), - ALERT_SMTP_SECURE: BoolEnv.optional(), - ALERT_SMTP_USER: z.string().optional(), - ALERT_SMTP_PASSWORD: z.string().optional(), - ALERT_RATE_LIMITER_EMISSION_INTERVAL: z.coerce.number().int().default(2_500), - ALERT_RATE_LIMITER_BURST_TOLERANCE: z.coerce.number().int().default(10_000), - ALERT_RATE_LIMITER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - ALERT_RATE_LIMITER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - ALERT_RATE_LIMITER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - ALERT_RATE_LIMITER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - ALERT_RATE_LIMITER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - ALERT_RATE_LIMITER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - ALERT_RATE_LIMITER_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - ALERT_RATE_LIMITER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - LOOPS_API_KEY: z.string().optional(), - MARQS_DISABLE_REBALANCING: BoolEnv.default(false), - MARQS_VISIBILITY_TIMEOUT_MS: z.coerce - .number() - .int() - .default(60 * 1000 * 15), - MARQS_SHARED_QUEUE_LIMIT: z.coerce.number().int().default(1000), - MARQS_MAXIMUM_QUEUE_PER_ENV_COUNT: z.coerce.number().int().default(50), - MARQS_DEV_QUEUE_LIMIT: z.coerce.number().int().default(1000), - MARQS_MAXIMUM_NACK_COUNT: z.coerce.number().int().default(64), - MARQS_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), - MARQS_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), - MARQS_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25), - MARQS_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), - MARQS_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), - MARQS_SHARED_WORKER_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(250), - MARQS_SHARED_WORKER_QUEUE_MAX_MESSAGE_COUNT: z.coerce.number().int().default(10), - - MARQS_SHARED_WORKER_QUEUE_EAGER_DEQUEUE_ENABLED: z.string().default("0"), - MARQS_WORKER_ENABLED: z.string().default("0"), - MARQS_WORKER_COUNT: z.coerce.number().int().default(2), - MARQS_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), - MARQS_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(5), - MARQS_WORKER_POLL_INTERVAL_MS: z.coerce.number().int().default(100), - MARQS_WORKER_IMMEDIATE_POLL_INTERVAL_MS: z.coerce.number().int().default(100), - MARQS_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - MARQS_SHARED_WORKER_QUEUE_COOLOFF_COUNT_THRESHOLD: z.coerce.number().int().default(10), - MARQS_SHARED_WORKER_QUEUE_COOLOFF_PERIOD_MS: z.coerce.number().int().default(5_000), - - PROD_TASK_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(), - - VERBOSE_GRAPHILE_LOGGING: z.string().default("false"), - V2_MARQS_ENABLED: z.string().default("0"), - V2_MARQS_CONSUMER_POOL_ENABLED: z.string().default("0"), - V2_MARQS_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10), - V2_MARQS_CONSUMER_POLL_INTERVAL_MS: z.coerce.number().int().default(1000), - V2_MARQS_QUEUE_SELECTION_COUNT: z.coerce.number().int().default(36), - V2_MARQS_VISIBILITY_TIMEOUT_MS: z.coerce - .number() - .int() - .default(60 * 1000 * 15), - V2_MARQS_DEFAULT_ENV_CONCURRENCY: z.coerce.number().int().default(100), - V2_MARQS_VERBOSE: z.string().default("0"), - V3_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"), - V2_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"), - /* Usage settings */ - USAGE_EVENT_URL: z.string().optional(), - PROD_USAGE_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(), - - CENTS_PER_RUN: z.coerce.number().default(0), - - EVENT_LOOP_MONITOR_ENABLED: z.string().default("1"), - MAXIMUM_LIVE_RELOADING_EVENTS: z.coerce.number().int().default(1000), - MAXIMUM_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000), - MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(10_000), - TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB - TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB - BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB - TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(262_144), // 256KB - - MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional(), - MAXIMUM_DEPLOYED_QUEUE_SIZE: z.coerce.number().int().optional(), - MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), - MAX_BATCH_AND_WAIT_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), - - REALTIME_STREAM_VERSION: z.enum(["v1", "v2"]).default("v1"), - REALTIME_STREAM_MAX_LENGTH: z.coerce.number().int().default(1000), - REALTIME_STREAM_TTL: z.coerce - .number() - .int() - .default(60 * 60 * 24), // 1 day in seconds - BATCH_METADATA_OPERATIONS_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), - BATCH_METADATA_OPERATIONS_FLUSH_ENABLED: z.string().default("1"), - BATCH_METADATA_OPERATIONS_FLUSH_LOGGING_ENABLED: z.string().default("1"), - - // Run Engine 2.0 - RUN_ENGINE_WORKER_COUNT: z.coerce.number().int().default(4), - RUN_ENGINE_TASKS_PER_WORKER: z.coerce.number().int().default(10), - RUN_ENGINE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(10), - RUN_ENGINE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(100), - RUN_ENGINE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(100), - RUN_ENGINE_TIMEOUT_PENDING_EXECUTING: z.coerce.number().int().default(60_000), - RUN_ENGINE_TIMEOUT_PENDING_CANCEL: z.coerce.number().int().default(60_000), - RUN_ENGINE_TIMEOUT_EXECUTING: z.coerce.number().int().default(60_000), - RUN_ENGINE_TIMEOUT_EXECUTING_WITH_WAITPOINTS: z.coerce.number().int().default(60_000), - RUN_ENGINE_TIMEOUT_SUSPENDED: z.coerce - .number() - .int() - .default(60_000 * 10), - RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: BoolEnv.default(false), - RUN_ENGINE_PARENT_QUEUE_LIMIT: z.coerce.number().int().default(1000), - RUN_ENGINE_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), - RUN_ENGINE_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), - RUN_ENGINE_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25), - RUN_ENGINE_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), - RUN_ENGINE_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), - RUN_ENGINE_RUN_QUEUE_SHARD_COUNT: z.coerce.number().int().default(4), - RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - RUN_ENGINE_RETRY_WARM_START_THRESHOLD_MS: z.coerce.number().int().default(30_000), - RUN_ENGINE_PROCESS_WORKER_QUEUE_DEBOUNCE_MS: z.coerce.number().int().default(200), - RUN_ENGINE_DEQUEUE_BLOCKING_TIMEOUT_SECONDS: z.coerce.number().int().default(10), - RUN_ENGINE_MASTER_QUEUE_CONSUMERS_INTERVAL_MS: z.coerce.number().int().default(1000), - RUN_ENGINE_MASTER_QUEUE_COOLOFF_PERIOD_MS: z.coerce.number().int().default(10_000), - RUN_ENGINE_MASTER_QUEUE_COOLOFF_COUNT_THRESHOLD: z.coerce.number().int().default(10), - RUN_ENGINE_MASTER_QUEUE_CONSUMER_DEQUEUE_COUNT: z.coerce.number().int().default(10), - RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_SCHEDULE: z.string().optional(), - RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_SCHEDULE: z.string().optional(), - RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_JITTER_IN_MS: z.coerce.number().int().optional(), - RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_JITTER_IN_MS: z.coerce.number().int().optional(), - - RUN_ENGINE_RUN_LOCK_DURATION: z.coerce.number().int().default(5000), - RUN_ENGINE_RUN_LOCK_AUTOMATIC_EXTENSION_THRESHOLD: z.coerce.number().int().default(1000), - RUN_ENGINE_RUN_LOCK_MAX_RETRIES: z.coerce.number().int().default(10), - RUN_ENGINE_RUN_LOCK_BASE_DELAY: z.coerce.number().int().default(100), - RUN_ENGINE_RUN_LOCK_MAX_DELAY: z.coerce.number().int().default(3000), - RUN_ENGINE_RUN_LOCK_BACKOFF_MULTIPLIER: z.coerce.number().default(1.8), - RUN_ENGINE_RUN_LOCK_JITTER_FACTOR: z.coerce.number().default(0.15), - RUN_ENGINE_RUN_LOCK_MAX_TOTAL_WAIT_TIME: z.coerce.number().int().default(15000), - - RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_MAX_COUNT: z.coerce.number().int().default(12), - RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_MAX_DELAY_MS: z.coerce - .number() - .int() - .default(60_000 * 60 * 6), - RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_INITIAL_DELAY_MS: z.coerce.number().int().default(60_000), - RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_FACTOR: z.coerce.number().default(2), - - RUN_ENGINE_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - RUN_ENGINE_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - RUN_ENGINE_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - RUN_ENGINE_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - RUN_ENGINE_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - RUN_ENGINE_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - RUN_ENGINE_WORKER_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - - RUN_ENGINE_RUN_QUEUE_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - RUN_ENGINE_RUN_QUEUE_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - RUN_ENGINE_RUN_QUEUE_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - RUN_ENGINE_RUN_QUEUE_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - RUN_ENGINE_RUN_QUEUE_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - RUN_ENGINE_RUN_QUEUE_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - RUN_ENGINE_RUN_QUEUE_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - - RUN_ENGINE_RUN_LOCK_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - RUN_ENGINE_RUN_LOCK_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - RUN_ENGINE_RUN_LOCK_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - RUN_ENGINE_RUN_LOCK_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - RUN_ENGINE_RUN_LOCK_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - RUN_ENGINE_RUN_LOCK_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - RUN_ENGINE_RUN_LOCK_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - - RUN_ENGINE_DEV_PRESENCE_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - RUN_ENGINE_DEV_PRESENCE_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - RUN_ENGINE_DEV_PRESENCE_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - RUN_ENGINE_DEV_PRESENCE_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - RUN_ENGINE_DEV_PRESENCE_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - RUN_ENGINE_DEV_PRESENCE_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - RUN_ENGINE_DEV_PRESENCE_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - - //API Rate limiting - /** - * @example "60s" - * @example "1m" - * @example "1h" - * @example "1d" - * @example "1000ms" - * @example "1000s" - */ - RUN_ENGINE_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds - RUN_ENGINE_RATE_LIMIT_MAX: z.coerce.number().int().default(1200), // allow bursts of 750 requests - RUN_ENGINE_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(400), // refix 250 tokens every 10 seconds - RUN_ENGINE_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"), - RUN_ENGINE_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"), - RUN_ENGINE_RATE_LIMIT_LIMITER_LOGS_ENABLED: z.string().default("0"), - - RUN_ENGINE_RELEASE_CONCURRENCY_ENABLED: z.string().default("0"), - RUN_ENGINE_RELEASE_CONCURRENCY_DISABLE_CONSUMERS: z.string().default("0"), - RUN_ENGINE_RELEASE_CONCURRENCY_MAX_TOKENS_RATIO: z.coerce.number().default(1), - RUN_ENGINE_RELEASE_CONCURRENCY_RELEASINGS_MAX_AGE: z.coerce - .number() - .int() - .default(60_000 * 30), - RUN_ENGINE_RELEASE_CONCURRENCY_RELEASINGS_POLL_INTERVAL: z.coerce.number().int().default(60_000), - RUN_ENGINE_RELEASE_CONCURRENCY_MAX_RETRIES: z.coerce.number().int().default(3), - RUN_ENGINE_RELEASE_CONCURRENCY_CONSUMERS_COUNT: z.coerce.number().int().default(1), - RUN_ENGINE_RELEASE_CONCURRENCY_POLL_INTERVAL: z.coerce.number().int().default(500), - RUN_ENGINE_RELEASE_CONCURRENCY_BATCH_SIZE: z.coerce.number().int().default(10), - - RUN_ENGINE_WORKER_ENABLED: z.string().default("1"), - RUN_ENGINE_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - RUN_ENGINE_RUN_QUEUE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - - /** How long should the presence ttl last */ - DEV_PRESENCE_SSE_TIMEOUT: z.coerce.number().int().default(30_000), - DEV_PRESENCE_TTL_MS: z.coerce.number().int().default(5_000), - DEV_PRESENCE_POLL_MS: z.coerce.number().int().default(1_000), - /** How many ms to wait until dequeuing again, if there was a run last time */ - DEV_DEQUEUE_INTERVAL_WITH_RUN: z.coerce.number().int().default(250), - /** How many ms to wait until dequeuing again, if there was no run last time */ - DEV_DEQUEUE_INTERVAL_WITHOUT_RUN: z.coerce.number().int().default(1_000), - /** The max number of runs per API call that we'll dequeue in DEV */ - DEV_DEQUEUE_MAX_RUNS_PER_PULL: z.coerce.number().int().default(10), - - /** The maximum concurrent local run processes executing at once in dev */ - DEV_MAX_CONCURRENT_RUNS: z.coerce.number().int().default(25), - - /** The CLI should connect to this for dev runs */ - DEV_ENGINE_URL: z.string().default(process.env.APP_ORIGIN ?? "http://localhost:3030"), - - LEGACY_RUN_ENGINE_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), - LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), - LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(1), - LEGACY_RUN_ENGINE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - LEGACY_RUN_ENGINE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), - LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), - LEGACY_RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - LEGACY_RUN_ENGINE_WORKER_LOG_LEVEL: z - .enum(["log", "error", "warn", "info", "debug"]) - .default("info"), - - LEGACY_RUN_ENGINE_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - LEGACY_RUN_ENGINE_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - LEGACY_RUN_ENGINE_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - LEGACY_RUN_ENGINE_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - LEGACY_RUN_ENGINE_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - LEGACY_RUN_ENGINE_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - LEGACY_RUN_ENGINE_WORKER_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - LEGACY_RUN_ENGINE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_BATCH_SIZE: z.coerce.number().int().default(100), - LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_BATCH_STAGGER_MS: z.coerce.number().int().default(1_000), - - COMMON_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), - COMMON_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), - COMMON_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), - COMMON_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - COMMON_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), - COMMON_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), - COMMON_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - COMMON_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - - COMMON_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - COMMON_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - COMMON_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - COMMON_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - COMMON_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - COMMON_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - COMMON_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - COMMON_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - BATCH_TRIGGER_PROCESS_JOB_VISIBILITY_TIMEOUT_MS: z.coerce - .number() - .int() - .default(60_000 * 5), // 5 minutes - - BATCH_TRIGGER_CACHED_RUNS_CHECK_ENABLED: BoolEnv.default(false), - - BATCH_TRIGGER_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), - BATCH_TRIGGER_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), - BATCH_TRIGGER_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), - BATCH_TRIGGER_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - BATCH_TRIGGER_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), - BATCH_TRIGGER_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), - BATCH_TRIGGER_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - BATCH_TRIGGER_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - - BATCH_TRIGGER_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - BATCH_TRIGGER_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - BATCH_TRIGGER_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - BATCH_TRIGGER_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - BATCH_TRIGGER_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - BATCH_TRIGGER_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - BATCH_TRIGGER_WORKER_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - BATCH_TRIGGER_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - ADMIN_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), - ADMIN_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), - ADMIN_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), - ADMIN_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - ADMIN_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), - ADMIN_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), - ADMIN_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - ADMIN_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - - ADMIN_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - ADMIN_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - ADMIN_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - ADMIN_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - ADMIN_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - ADMIN_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - ADMIN_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - ADMIN_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - ALERTS_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), - ALERTS_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), - ALERTS_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), - ALERTS_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - ALERTS_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(100), - ALERTS_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), - ALERTS_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), - ALERTS_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - - ALERTS_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - ALERTS_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - ALERTS_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - ALERTS_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - ALERTS_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - ALERTS_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - ALERTS_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - ALERTS_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - SCHEDULE_ENGINE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - SCHEDULE_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), - SCHEDULE_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), - SCHEDULE_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), - SCHEDULE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), - SCHEDULE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), - SCHEDULE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), - SCHEDULE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(30_000), - SCHEDULE_WORKER_DISTRIBUTION_WINDOW_SECONDS: z.coerce.number().int().default(30), - - SCHEDULE_WORKER_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - SCHEDULE_WORKER_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - SCHEDULE_WORKER_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - SCHEDULE_WORKER_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - SCHEDULE_WORKER_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - SCHEDULE_WORKER_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - SCHEDULE_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - SCHEDULE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), - - TASK_EVENT_PARTITIONING_ENABLED: z.string().default("0"), - TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS: z.coerce.number().int().default(60), // 1 minute - - QUEUE_SSE_AUTORELOAD_INTERVAL_MS: z.coerce.number().int().default(5_000), - QUEUE_SSE_AUTORELOAD_TIMEOUT_MS: z.coerce.number().int().default(60_000), - - SLACK_BOT_TOKEN: z.string().optional(), - SLACK_SIGNUP_REASON_CHANNEL_ID: z.string().optional(), - - // kapa.ai - KAPA_AI_WEBSITE_ID: z.string().optional(), - - // BetterStack - BETTERSTACK_API_KEY: z.string().optional(), - BETTERSTACK_STATUS_PAGE_ID: z.string().optional(), - - RUN_REPLICATION_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - RUN_REPLICATION_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - RUN_REPLICATION_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - RUN_REPLICATION_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - RUN_REPLICATION_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - RUN_REPLICATION_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - RUN_REPLICATION_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), - - RUN_REPLICATION_CLICKHOUSE_URL: z.string().optional(), - RUN_REPLICATION_ENABLED: z.string().default("0"), - RUN_REPLICATION_SLOT_NAME: z.string().default("task_runs_to_clickhouse_v1"), - RUN_REPLICATION_PUBLICATION_NAME: z.string().default("task_runs_to_clickhouse_v1_publication"), - RUN_REPLICATION_MAX_FLUSH_CONCURRENCY: z.coerce.number().int().default(2), - RUN_REPLICATION_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), - RUN_REPLICATION_FLUSH_BATCH_SIZE: z.coerce.number().int().default(100), - RUN_REPLICATION_LEADER_LOCK_TIMEOUT_MS: z.coerce.number().int().default(30_000), - RUN_REPLICATION_LEADER_LOCK_EXTEND_INTERVAL_MS: z.coerce.number().int().default(10_000), - RUN_REPLICATION_ACK_INTERVAL_SECONDS: z.coerce.number().int().default(10), - RUN_REPLICATION_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - RUN_REPLICATION_CLICKHOUSE_LOG_LEVEL: z - .enum(["log", "error", "warn", "info", "debug"]) - .default("info"), - RUN_REPLICATION_LEADER_LOCK_ADDITIONAL_TIME_MS: z.coerce.number().int().default(10_000), - RUN_REPLICATION_LEADER_LOCK_RETRY_INTERVAL_MS: z.coerce.number().int().default(500), - RUN_REPLICATION_WAIT_FOR_ASYNC_INSERT: z.string().default("0"), - RUN_REPLICATION_KEEP_ALIVE_ENABLED: z.string().default("0"), - RUN_REPLICATION_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), - RUN_REPLICATION_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), - // Retry configuration for insert operations - RUN_REPLICATION_INSERT_MAX_RETRIES: z.coerce.number().int().default(3), - RUN_REPLICATION_INSERT_BASE_DELAY_MS: z.coerce.number().int().default(100), - RUN_REPLICATION_INSERT_MAX_DELAY_MS: z.coerce.number().int().default(2000), - RUN_REPLICATION_INSERT_STRATEGY: z.enum(["insert", "insert_async"]).default("insert"), - - // Clickhouse - CLICKHOUSE_URL: z.string(), - CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"), - CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), - CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), - CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"), - - // Bootstrap - TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"), - TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME: z.string().optional(), - TRIGGER_BOOTSTRAP_WORKER_TOKEN_PATH: z.string().optional(), - - // Machine presets - MACHINE_PRESETS_OVERRIDE_PATH: z.string().optional(), - - // CLI package tag (e.g. "latest", "v4-beta", "4.0.0") - used for setup commands - TRIGGER_CLI_TAG: z.string().default("latest"), - - HEALTHCHECK_DATABASE_DISABLED: z.string().default("0"), - - REQUEST_IDEMPOTENCY_REDIS_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_HOST), - REQUEST_IDEMPOTENCY_REDIS_READER_HOST: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_READER_HOST), - REQUEST_IDEMPOTENCY_REDIS_READER_PORT: z.coerce - .number() - .optional() - .transform( - (v) => - v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) - ), - REQUEST_IDEMPOTENCY_REDIS_PORT: z.coerce - .number() - .optional() - .transform((v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined)), - REQUEST_IDEMPOTENCY_REDIS_USERNAME: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_USERNAME), - REQUEST_IDEMPOTENCY_REDIS_PASSWORD: z - .string() - .optional() - .transform((v) => v ?? process.env.REDIS_PASSWORD), - REQUEST_IDEMPOTENCY_REDIS_TLS_DISABLED: z - .string() - .default(process.env.REDIS_TLS_DISABLED ?? "false"), - - REQUEST_IDEMPOTENCY_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), - - REQUEST_IDEMPOTENCY_TTL_IN_MS: z.coerce - .number() - .int() - .default(60_000 * 60 * 24), - - // Bulk action - BULK_ACTION_BATCH_SIZE: z.coerce.number().int().default(100), - BULK_ACTION_BATCH_DELAY_MS: z.coerce.number().int().default(200), - BULK_ACTION_SUBBATCH_CONCURRENCY: z.coerce.number().int().default(5), - - // AI Run Filter - AI_RUN_FILTER_MODEL: z.string().optional(), - - EVENT_LOOP_MONITOR_THRESHOLD_MS: z.coerce.number().int().default(100), - - VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(), -}); +const GithubAppEnvSchema = z.union([ + z.object({ + GITHUB_APP_ENABLED: z.literal("1"), + GITHUB_APP_ID: z.string(), + GITHUB_APP_PRIVATE_KEY: z.string(), + GITHUB_APP_WEBHOOK_SECRET: z.string(), + GITHUB_APP_SLUG: z.string(), + }), + z.object({ + GITHUB_APP_ENABLED: z.literal("0"), + }), +]); + +const EnvironmentSchema = z + .object({ + NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]), + DATABASE_URL: z + .string() + .refine( + isValidDatabaseUrl, + "DATABASE_URL is invalid, for details please check the additional output above this message." + ), + DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10), + DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60), + DATABASE_CONNECTION_TIMEOUT: z.coerce.number().int().default(20), + DIRECT_URL: z + .string() + .refine( + isValidDatabaseUrl, + "DIRECT_URL is invalid, for details please check the additional output above this message." + ), + DATABASE_READ_REPLICA_URL: z.string().optional(), + SESSION_SECRET: z.string(), + MAGIC_LINK_SECRET: 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.") + .optional(), + ADMIN_EMAILS: z.string().refine(isValidRegex, "ADMIN_EMAILS must be a valid regex.").optional(), + REMIX_APP_PORT: z.string().optional(), + LOGIN_ORIGIN: z.string().default("http://localhost:3030"), + APP_ORIGIN: z.string().default("http://localhost:3030"), + API_ORIGIN: z.string().optional(), + STREAM_ORIGIN: z.string().optional(), + ELECTRIC_ORIGIN: z.string().default("http://localhost:3060"), + // A comma separated list of electric origins to shard into different electric instances by environmentId + // example: "http://localhost:3060,http://localhost:3061,http://localhost:3062" + ELECTRIC_ORIGIN_SHARDS: z.string().optional(), + APP_ENV: z.string().default(process.env.NODE_ENV), + SERVICE_NAME: z.string().default("trigger.dev webapp"), + POSTHOG_PROJECT_KEY: z.string().default("phc_LFH7kJiGhdIlnO22hTAKgHpaKhpM8gkzWAFvHmf5vfS"), + TRIGGER_TELEMETRY_DISABLED: z.string().optional(), + AUTH_GITHUB_CLIENT_ID: z.string().optional(), + AUTH_GITHUB_CLIENT_SECRET: z.string().optional(), + EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), + FROM_EMAIL: z.string().optional(), + REPLY_TO_EMAIL: z.string().optional(), + RESEND_API_KEY: z.string().optional(), + SMTP_HOST: z.string().optional(), + SMTP_PORT: z.coerce.number().optional(), + SMTP_SECURE: BoolEnv.optional(), + SMTP_USER: z.string().optional(), + SMTP_PASSWORD: z.string().optional(), + + // GitHub App + GITHUB_APP_ID: z.string(), + GITHUB_APP_PRIVATE_KEY: z.string(), + GITHUB_APP_WEBHOOK_SECRET: z.string(), + GITHUB_APP_SLUG: z.string(), + + PLAIN_API_KEY: z.string().optional(), + WORKER_SCHEMA: z.string().default("graphile_worker"), + WORKER_CONCURRENCY: z.coerce.number().int().default(10), + WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + WORKER_ENABLED: z.string().default("true"), + GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000), + DISABLE_SSE: z.string().optional(), + OPENAI_API_KEY: z.string().optional(), + + // Redis options + REDIS_HOST: z.string().optional(), + REDIS_READER_HOST: z.string().optional(), + REDIS_READER_PORT: z.coerce.number().optional(), + REDIS_PORT: z.coerce.number().optional(), + REDIS_USERNAME: z.string().optional(), + REDIS_PASSWORD: z.string().optional(), + REDIS_TLS_DISABLED: z.string().optional(), + + RATE_LIMIT_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + RATE_LIMIT_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + RATE_LIMIT_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + RATE_LIMIT_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + RATE_LIMIT_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + RATE_LIMIT_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + RATE_LIMIT_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), + RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + CACHE_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + CACHE_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + CACHE_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + CACHE_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + CACHE_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + CACHE_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + CACHE_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), + CACHE_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + REALTIME_STREAMS_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + REALTIME_STREAMS_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + REALTIME_STREAMS_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + REALTIME_STREAMS_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + REALTIME_STREAMS_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + REALTIME_STREAMS_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + REALTIME_STREAMS_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + REALTIME_STREAMS_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + REALTIME_MAXIMUM_CREATED_AT_FILTER_AGE_IN_MS: z.coerce + .number() + .int() + .default(24 * 60 * 60 * 1000), // 1 day in milliseconds + + PUBSUB_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + PUBSUB_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + PUBSUB_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + PUBSUB_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + PUBSUB_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + PUBSUB_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + PUBSUB_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), + PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(100), + DEFAULT_ENV_EXECUTION_CONCURRENCY_BURST_FACTOR: z.coerce.number().default(1.0), + DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(300), + DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1), + + //API Rate limiting + /** + * @example "60s" + * @example "1m" + * @example "1h" + * @example "1d" + * @example "1000ms" + * @example "1000s" + */ + API_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds + API_RATE_LIMIT_MAX: z.coerce.number().int().default(750), // allow bursts of 750 requests + API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refix 250 tokens every 10 seconds + API_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"), + API_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"), + API_RATE_LIMIT_LIMITER_LOGS_ENABLED: z.string().default("0"), + + API_RATE_LIMIT_JWT_WINDOW: z.string().default("1m"), + API_RATE_LIMIT_JWT_TOKENS: z.coerce.number().int().default(60), + + //v3 + PROVIDER_SECRET: z.string().default("provider-secret"), + COORDINATOR_SECRET: z.string().default("coordinator-secret"), + DEPOT_TOKEN: z.string().optional(), + DEPOT_ORG_ID: z.string().optional(), + DEPOT_REGION: z.string().default("us-east-1"), + + // Deployment registry (v3) + DEPLOY_REGISTRY_HOST: z.string().min(1), + DEPLOY_REGISTRY_USERNAME: z.string().optional(), + DEPLOY_REGISTRY_PASSWORD: z.string().optional(), + DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"), + DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2" + DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(), + DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(), + + // Deployment registry (v4) - falls back to v3 registry if not specified + V4_DEPLOY_REGISTRY_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_HOST) + .pipe(z.string().min(1)), // Ensure final type is required string + V4_DEPLOY_REGISTRY_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_USERNAME), + V4_DEPLOY_REGISTRY_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_PASSWORD), + V4_DEPLOY_REGISTRY_NAMESPACE: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_NAMESPACE) + .pipe(z.string().min(1).default("trigger")), // Ensure final type is required string + V4_DEPLOY_REGISTRY_ECR_TAGS: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_TAGS), + V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN), + V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z + .string() + .optional() + .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID), + + DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"), + DEPLOY_TIMEOUT_MS: z.coerce + .number() + .int() + .default(60 * 1000 * 8), // 8 minutes + + OBJECT_STORE_BASE_URL: z.string().optional(), + OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(), + OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(), + OBJECT_STORE_REGION: z.string().optional(), + OBJECT_STORE_SERVICE: z.string().default("s3"), + EVENTS_BATCH_SIZE: z.coerce.number().int().default(100), + EVENTS_BATCH_INTERVAL: z.coerce.number().int().default(1000), + EVENTS_DEFAULT_LOG_RETENTION: z.coerce.number().int().default(7), + EVENTS_MIN_CONCURRENCY: z.coerce.number().int().default(1), + EVENTS_MAX_CONCURRENCY: z.coerce.number().int().default(10), + EVENTS_MAX_BATCH_SIZE: z.coerce.number().int().default(500), + EVENTS_MEMORY_PRESSURE_THRESHOLD: z.coerce.number().int().default(5000), + EVENTS_LOAD_SHEDDING_THRESHOLD: z.coerce.number().int().default(100000), + EVENTS_LOAD_SHEDDING_ENABLED: z.string().default("1"), + SHARED_QUEUE_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10), + SHARED_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(100), + SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS: z.coerce.number().int().default(100), + SHARED_QUEUE_CONSUMER_EMIT_RESUME_DEPENDENCY_TIMEOUT_MS: z.coerce.number().int().default(1000), + SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE: z.coerce.number().int().default(25), + + MANAGED_WORKER_SECRET: z.string().default("managed-secret"), + + // Development OTEL environment variables + DEV_OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(), + // If this is set to 1, then the below variables are used to configure the batch processor for spans and logs + DEV_OTEL_BATCH_PROCESSING_ENABLED: z.string().default("0"), + DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), + DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS: z.string().default("200"), + DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), + DEV_OTEL_SPAN_MAX_QUEUE_SIZE: z.string().default("512"), + DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), + DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS: z.string().default("200"), + DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), + DEV_OTEL_LOG_MAX_QUEUE_SIZE: z.string().default("512"), + + PROD_OTEL_BATCH_PROCESSING_ENABLED: z.string().default("0"), + PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), + PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS: z.string().default("200"), + PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), + PROD_OTEL_SPAN_MAX_QUEUE_SIZE: z.string().default("512"), + PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), + PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS: z.string().default("200"), + PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), + PROD_OTEL_LOG_MAX_QUEUE_SIZE: z.string().default("512"), + + TRIGGER_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: z.string().default("1024"), + TRIGGER_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT: z.string().default("1024"), + TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: z.string().default("131072"), + TRIGGER_OTEL_LOG_ATTRIBUTE_VALUE_LENGTH_LIMIT: z.string().default("131072"), + TRIGGER_OTEL_SPAN_EVENT_COUNT_LIMIT: z.string().default("10"), + TRIGGER_OTEL_LINK_COUNT_LIMIT: z.string().default("2"), + TRIGGER_OTEL_ATTRIBUTE_PER_LINK_COUNT_LIMIT: z.string().default("10"), + TRIGGER_OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT: z.string().default("10"), + + CHECKPOINT_THRESHOLD_IN_MS: z.coerce.number().int().default(30000), + + // Internal OTEL environment variables + INTERNAL_OTEL_TRACE_EXPORTER_URL: z.string().optional(), + INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADERS: z.string().optional(), + INTERNAL_OTEL_TRACE_LOGGING_ENABLED: z.string().default("1"), + // this means 1/20 traces or 5% of traces will be sampled (sampled = recorded) + INTERNAL_OTEL_TRACE_SAMPLING_RATE: z.string().default("20"), + INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED: z.string().default("0"), + INTERNAL_OTEL_TRACE_DISABLED: z.string().default("0"), + + INTERNAL_OTEL_LOG_EXPORTER_URL: z.string().optional(), + INTERNAL_OTEL_METRIC_EXPORTER_URL: z.string().optional(), + INTERNAL_OTEL_METRIC_EXPORTER_AUTH_HEADERS: z.string().optional(), + INTERNAL_OTEL_METRIC_EXPORTER_ENABLED: z.string().default("0"), + INTERNAL_OTEL_METRIC_EXPORTER_INTERVAL_MS: z.coerce.number().int().default(30_000), + INTERNAL_OTEL_HOST_METRICS_ENABLED: BoolEnv.default(true), + INTERNAL_OTEL_NODEJS_METRICS_ENABLED: BoolEnv.default(true), + INTERNAL_OTEL_ADDITIONAL_DETECTORS_ENABLED: BoolEnv.default(true), + + ORG_SLACK_INTEGRATION_CLIENT_ID: z.string().optional(), + ORG_SLACK_INTEGRATION_CLIENT_SECRET: z.string().optional(), + + /** These enable the alerts feature in v3 */ + ALERT_EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), + ALERT_FROM_EMAIL: z.string().optional(), + ALERT_REPLY_TO_EMAIL: z.string().optional(), + ALERT_RESEND_API_KEY: z.string().optional(), + ALERT_SMTP_HOST: z.string().optional(), + ALERT_SMTP_PORT: z.coerce.number().optional(), + ALERT_SMTP_SECURE: BoolEnv.optional(), + ALERT_SMTP_USER: z.string().optional(), + ALERT_SMTP_PASSWORD: z.string().optional(), + ALERT_RATE_LIMITER_EMISSION_INTERVAL: z.coerce.number().int().default(2_500), + ALERT_RATE_LIMITER_BURST_TOLERANCE: z.coerce.number().int().default(10_000), + ALERT_RATE_LIMITER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + ALERT_RATE_LIMITER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + ALERT_RATE_LIMITER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + ALERT_RATE_LIMITER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + ALERT_RATE_LIMITER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + ALERT_RATE_LIMITER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + ALERT_RATE_LIMITER_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + ALERT_RATE_LIMITER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + LOOPS_API_KEY: z.string().optional(), + MARQS_DISABLE_REBALANCING: BoolEnv.default(false), + MARQS_VISIBILITY_TIMEOUT_MS: z.coerce + .number() + .int() + .default(60 * 1000 * 15), + MARQS_SHARED_QUEUE_LIMIT: z.coerce.number().int().default(1000), + MARQS_MAXIMUM_QUEUE_PER_ENV_COUNT: z.coerce.number().int().default(50), + MARQS_DEV_QUEUE_LIMIT: z.coerce.number().int().default(1000), + MARQS_MAXIMUM_NACK_COUNT: z.coerce.number().int().default(64), + MARQS_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), + MARQS_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), + MARQS_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25), + MARQS_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), + MARQS_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), + MARQS_SHARED_WORKER_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(250), + MARQS_SHARED_WORKER_QUEUE_MAX_MESSAGE_COUNT: z.coerce.number().int().default(10), + + MARQS_SHARED_WORKER_QUEUE_EAGER_DEQUEUE_ENABLED: z.string().default("0"), + MARQS_WORKER_ENABLED: z.string().default("0"), + MARQS_WORKER_COUNT: z.coerce.number().int().default(2), + MARQS_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), + MARQS_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(5), + MARQS_WORKER_POLL_INTERVAL_MS: z.coerce.number().int().default(100), + MARQS_WORKER_IMMEDIATE_POLL_INTERVAL_MS: z.coerce.number().int().default(100), + MARQS_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + MARQS_SHARED_WORKER_QUEUE_COOLOFF_COUNT_THRESHOLD: z.coerce.number().int().default(10), + MARQS_SHARED_WORKER_QUEUE_COOLOFF_PERIOD_MS: z.coerce.number().int().default(5_000), + + PROD_TASK_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(), + + VERBOSE_GRAPHILE_LOGGING: z.string().default("false"), + V2_MARQS_ENABLED: z.string().default("0"), + V2_MARQS_CONSUMER_POOL_ENABLED: z.string().default("0"), + V2_MARQS_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10), + V2_MARQS_CONSUMER_POLL_INTERVAL_MS: z.coerce.number().int().default(1000), + V2_MARQS_QUEUE_SELECTION_COUNT: z.coerce.number().int().default(36), + V2_MARQS_VISIBILITY_TIMEOUT_MS: z.coerce + .number() + .int() + .default(60 * 1000 * 15), + V2_MARQS_DEFAULT_ENV_CONCURRENCY: z.coerce.number().int().default(100), + V2_MARQS_VERBOSE: z.string().default("0"), + V3_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"), + V2_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"), + /* Usage settings */ + USAGE_EVENT_URL: z.string().optional(), + PROD_USAGE_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(), + + CENTS_PER_RUN: z.coerce.number().default(0), + + EVENT_LOOP_MONITOR_ENABLED: z.string().default("1"), + MAXIMUM_LIVE_RELOADING_EVENTS: z.coerce.number().int().default(1000), + MAXIMUM_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000), + MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(10_000), + TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB + TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB + BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB + TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(262_144), // 256KB + + MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional(), + MAXIMUM_DEPLOYED_QUEUE_SIZE: z.coerce.number().int().optional(), + MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), + MAX_BATCH_AND_WAIT_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), + + REALTIME_STREAM_VERSION: z.enum(["v1", "v2"]).default("v1"), + REALTIME_STREAM_MAX_LENGTH: z.coerce.number().int().default(1000), + REALTIME_STREAM_TTL: z.coerce + .number() + .int() + .default(60 * 60 * 24), // 1 day in seconds + BATCH_METADATA_OPERATIONS_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), + BATCH_METADATA_OPERATIONS_FLUSH_ENABLED: z.string().default("1"), + BATCH_METADATA_OPERATIONS_FLUSH_LOGGING_ENABLED: z.string().default("1"), + + // Run Engine 2.0 + RUN_ENGINE_WORKER_COUNT: z.coerce.number().int().default(4), + RUN_ENGINE_TASKS_PER_WORKER: z.coerce.number().int().default(10), + RUN_ENGINE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(10), + RUN_ENGINE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(100), + RUN_ENGINE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(100), + RUN_ENGINE_TIMEOUT_PENDING_EXECUTING: z.coerce.number().int().default(60_000), + RUN_ENGINE_TIMEOUT_PENDING_CANCEL: z.coerce.number().int().default(60_000), + RUN_ENGINE_TIMEOUT_EXECUTING: z.coerce.number().int().default(60_000), + RUN_ENGINE_TIMEOUT_EXECUTING_WITH_WAITPOINTS: z.coerce.number().int().default(60_000), + RUN_ENGINE_TIMEOUT_SUSPENDED: z.coerce + .number() + .int() + .default(60_000 * 10), + RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: BoolEnv.default(false), + RUN_ENGINE_PARENT_QUEUE_LIMIT: z.coerce.number().int().default(1000), + RUN_ENGINE_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), + RUN_ENGINE_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), + RUN_ENGINE_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25), + RUN_ENGINE_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), + RUN_ENGINE_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), + RUN_ENGINE_RUN_QUEUE_SHARD_COUNT: z.coerce.number().int().default(4), + RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + RUN_ENGINE_RETRY_WARM_START_THRESHOLD_MS: z.coerce.number().int().default(30_000), + RUN_ENGINE_PROCESS_WORKER_QUEUE_DEBOUNCE_MS: z.coerce.number().int().default(200), + RUN_ENGINE_DEQUEUE_BLOCKING_TIMEOUT_SECONDS: z.coerce.number().int().default(10), + RUN_ENGINE_MASTER_QUEUE_CONSUMERS_INTERVAL_MS: z.coerce.number().int().default(1000), + RUN_ENGINE_MASTER_QUEUE_COOLOFF_PERIOD_MS: z.coerce.number().int().default(10_000), + RUN_ENGINE_MASTER_QUEUE_COOLOFF_COUNT_THRESHOLD: z.coerce.number().int().default(10), + RUN_ENGINE_MASTER_QUEUE_CONSUMER_DEQUEUE_COUNT: z.coerce.number().int().default(10), + RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_SCHEDULE: z.string().optional(), + RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_SCHEDULE: z.string().optional(), + RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_JITTER_IN_MS: z.coerce.number().int().optional(), + RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_JITTER_IN_MS: z.coerce.number().int().optional(), + + RUN_ENGINE_RUN_LOCK_DURATION: z.coerce.number().int().default(5000), + RUN_ENGINE_RUN_LOCK_AUTOMATIC_EXTENSION_THRESHOLD: z.coerce.number().int().default(1000), + RUN_ENGINE_RUN_LOCK_MAX_RETRIES: z.coerce.number().int().default(10), + RUN_ENGINE_RUN_LOCK_BASE_DELAY: z.coerce.number().int().default(100), + RUN_ENGINE_RUN_LOCK_MAX_DELAY: z.coerce.number().int().default(3000), + RUN_ENGINE_RUN_LOCK_BACKOFF_MULTIPLIER: z.coerce.number().default(1.8), + RUN_ENGINE_RUN_LOCK_JITTER_FACTOR: z.coerce.number().default(0.15), + RUN_ENGINE_RUN_LOCK_MAX_TOTAL_WAIT_TIME: z.coerce.number().int().default(15000), + + RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_MAX_COUNT: z.coerce.number().int().default(12), + RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_MAX_DELAY_MS: z.coerce + .number() + .int() + .default(60_000 * 60 * 6), + RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_INITIAL_DELAY_MS: z.coerce + .number() + .int() + .default(60_000), + RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_FACTOR: z.coerce.number().default(2), + + RUN_ENGINE_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + RUN_ENGINE_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + RUN_ENGINE_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + RUN_ENGINE_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + RUN_ENGINE_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + RUN_ENGINE_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + RUN_ENGINE_WORKER_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + RUN_ENGINE_RUN_QUEUE_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + RUN_ENGINE_RUN_QUEUE_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + RUN_ENGINE_RUN_QUEUE_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + RUN_ENGINE_RUN_QUEUE_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + RUN_ENGINE_RUN_QUEUE_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + RUN_ENGINE_RUN_QUEUE_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + RUN_ENGINE_RUN_QUEUE_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + RUN_ENGINE_RUN_LOCK_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + RUN_ENGINE_RUN_LOCK_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + RUN_ENGINE_RUN_LOCK_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + RUN_ENGINE_RUN_LOCK_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + RUN_ENGINE_RUN_LOCK_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + RUN_ENGINE_RUN_LOCK_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + RUN_ENGINE_RUN_LOCK_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + RUN_ENGINE_DEV_PRESENCE_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + RUN_ENGINE_DEV_PRESENCE_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + RUN_ENGINE_DEV_PRESENCE_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + RUN_ENGINE_DEV_PRESENCE_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + RUN_ENGINE_DEV_PRESENCE_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + RUN_ENGINE_DEV_PRESENCE_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + RUN_ENGINE_DEV_PRESENCE_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + //API Rate limiting + /** + * @example "60s" + * @example "1m" + * @example "1h" + * @example "1d" + * @example "1000ms" + * @example "1000s" + */ + RUN_ENGINE_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds + RUN_ENGINE_RATE_LIMIT_MAX: z.coerce.number().int().default(1200), // allow bursts of 750 requests + RUN_ENGINE_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(400), // refix 250 tokens every 10 seconds + RUN_ENGINE_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"), + RUN_ENGINE_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"), + RUN_ENGINE_RATE_LIMIT_LIMITER_LOGS_ENABLED: z.string().default("0"), + + RUN_ENGINE_RELEASE_CONCURRENCY_ENABLED: z.string().default("0"), + RUN_ENGINE_RELEASE_CONCURRENCY_DISABLE_CONSUMERS: z.string().default("0"), + RUN_ENGINE_RELEASE_CONCURRENCY_MAX_TOKENS_RATIO: z.coerce.number().default(1), + RUN_ENGINE_RELEASE_CONCURRENCY_RELEASINGS_MAX_AGE: z.coerce + .number() + .int() + .default(60_000 * 30), + RUN_ENGINE_RELEASE_CONCURRENCY_RELEASINGS_POLL_INTERVAL: z.coerce + .number() + .int() + .default(60_000), + RUN_ENGINE_RELEASE_CONCURRENCY_MAX_RETRIES: z.coerce.number().int().default(3), + RUN_ENGINE_RELEASE_CONCURRENCY_CONSUMERS_COUNT: z.coerce.number().int().default(1), + RUN_ENGINE_RELEASE_CONCURRENCY_POLL_INTERVAL: z.coerce.number().int().default(500), + RUN_ENGINE_RELEASE_CONCURRENCY_BATCH_SIZE: z.coerce.number().int().default(10), + + RUN_ENGINE_WORKER_ENABLED: z.string().default("1"), + RUN_ENGINE_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + RUN_ENGINE_RUN_QUEUE_LOG_LEVEL: z + .enum(["log", "error", "warn", "info", "debug"]) + .default("info"), + + /** How long should the presence ttl last */ + DEV_PRESENCE_SSE_TIMEOUT: z.coerce.number().int().default(30_000), + DEV_PRESENCE_TTL_MS: z.coerce.number().int().default(5_000), + DEV_PRESENCE_POLL_MS: z.coerce.number().int().default(1_000), + /** How many ms to wait until dequeuing again, if there was a run last time */ + DEV_DEQUEUE_INTERVAL_WITH_RUN: z.coerce.number().int().default(250), + /** How many ms to wait until dequeuing again, if there was no run last time */ + DEV_DEQUEUE_INTERVAL_WITHOUT_RUN: z.coerce.number().int().default(1_000), + /** The max number of runs per API call that we'll dequeue in DEV */ + DEV_DEQUEUE_MAX_RUNS_PER_PULL: z.coerce.number().int().default(10), + + /** The maximum concurrent local run processes executing at once in dev */ + DEV_MAX_CONCURRENT_RUNS: z.coerce.number().int().default(25), + + /** The CLI should connect to this for dev runs */ + DEV_ENGINE_URL: z.string().default(process.env.APP_ORIGIN ?? "http://localhost:3030"), + + LEGACY_RUN_ENGINE_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(1), + LEGACY_RUN_ENGINE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + LEGACY_RUN_ENGINE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), + LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), + LEGACY_RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + LEGACY_RUN_ENGINE_WORKER_LOG_LEVEL: z + .enum(["log", "error", "warn", "info", "debug"]) + .default("info"), + + LEGACY_RUN_ENGINE_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + LEGACY_RUN_ENGINE_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + LEGACY_RUN_ENGINE_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + LEGACY_RUN_ENGINE_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + LEGACY_RUN_ENGINE_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + LEGACY_RUN_ENGINE_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + LEGACY_RUN_ENGINE_WORKER_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + LEGACY_RUN_ENGINE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_BATCH_SIZE: z.coerce.number().int().default(100), + LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_BATCH_STAGGER_MS: z.coerce.number().int().default(1_000), + + COMMON_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + COMMON_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + COMMON_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), + COMMON_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + COMMON_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), + COMMON_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), + COMMON_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + COMMON_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + + COMMON_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + COMMON_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + COMMON_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + COMMON_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + COMMON_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + COMMON_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + COMMON_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), + COMMON_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + BATCH_TRIGGER_PROCESS_JOB_VISIBILITY_TIMEOUT_MS: z.coerce + .number() + .int() + .default(60_000 * 5), // 5 minutes + + BATCH_TRIGGER_CACHED_RUNS_CHECK_ENABLED: BoolEnv.default(false), + + BATCH_TRIGGER_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + BATCH_TRIGGER_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + BATCH_TRIGGER_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), + BATCH_TRIGGER_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + BATCH_TRIGGER_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), + BATCH_TRIGGER_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), + BATCH_TRIGGER_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + BATCH_TRIGGER_WORKER_LOG_LEVEL: z + .enum(["log", "error", "warn", "info", "debug"]) + .default("info"), + + BATCH_TRIGGER_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + BATCH_TRIGGER_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + BATCH_TRIGGER_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + BATCH_TRIGGER_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + BATCH_TRIGGER_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + BATCH_TRIGGER_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + BATCH_TRIGGER_WORKER_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + BATCH_TRIGGER_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + ADMIN_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + ADMIN_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + ADMIN_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), + ADMIN_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + ADMIN_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), + ADMIN_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), + ADMIN_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + ADMIN_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + + ADMIN_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + ADMIN_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + ADMIN_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + ADMIN_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + ADMIN_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + ADMIN_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + ADMIN_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), + ADMIN_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + ALERTS_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + ALERTS_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + ALERTS_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), + ALERTS_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + ALERTS_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(100), + ALERTS_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), + ALERTS_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), + ALERTS_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + + ALERTS_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + ALERTS_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + ALERTS_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + ALERTS_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + ALERTS_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + ALERTS_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + ALERTS_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), + ALERTS_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + SCHEDULE_ENGINE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + SCHEDULE_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + SCHEDULE_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + SCHEDULE_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), + SCHEDULE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + SCHEDULE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), + SCHEDULE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), + SCHEDULE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(30_000), + SCHEDULE_WORKER_DISTRIBUTION_WINDOW_SECONDS: z.coerce.number().int().default(30), + + SCHEDULE_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + SCHEDULE_WORKER_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + SCHEDULE_WORKER_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + SCHEDULE_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + SCHEDULE_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + SCHEDULE_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + SCHEDULE_WORKER_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + SCHEDULE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + + TASK_EVENT_PARTITIONING_ENABLED: z.string().default("0"), + TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS: z.coerce.number().int().default(60), // 1 minute + + QUEUE_SSE_AUTORELOAD_INTERVAL_MS: z.coerce.number().int().default(5_000), + QUEUE_SSE_AUTORELOAD_TIMEOUT_MS: z.coerce.number().int().default(60_000), + + SLACK_BOT_TOKEN: z.string().optional(), + SLACK_SIGNUP_REASON_CHANNEL_ID: z.string().optional(), + + // kapa.ai + KAPA_AI_WEBSITE_ID: z.string().optional(), + + // BetterStack + BETTERSTACK_API_KEY: z.string().optional(), + BETTERSTACK_STATUS_PAGE_ID: z.string().optional(), + + RUN_REPLICATION_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + RUN_REPLICATION_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + RUN_REPLICATION_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + RUN_REPLICATION_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + RUN_REPLICATION_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + RUN_REPLICATION_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + RUN_REPLICATION_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + RUN_REPLICATION_CLICKHOUSE_URL: z.string().optional(), + RUN_REPLICATION_ENABLED: z.string().default("0"), + RUN_REPLICATION_SLOT_NAME: z.string().default("task_runs_to_clickhouse_v1"), + RUN_REPLICATION_PUBLICATION_NAME: z.string().default("task_runs_to_clickhouse_v1_publication"), + RUN_REPLICATION_MAX_FLUSH_CONCURRENCY: z.coerce.number().int().default(2), + RUN_REPLICATION_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), + RUN_REPLICATION_FLUSH_BATCH_SIZE: z.coerce.number().int().default(100), + RUN_REPLICATION_LEADER_LOCK_TIMEOUT_MS: z.coerce.number().int().default(30_000), + RUN_REPLICATION_LEADER_LOCK_EXTEND_INTERVAL_MS: z.coerce.number().int().default(10_000), + RUN_REPLICATION_ACK_INTERVAL_SECONDS: z.coerce.number().int().default(10), + RUN_REPLICATION_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + RUN_REPLICATION_CLICKHOUSE_LOG_LEVEL: z + .enum(["log", "error", "warn", "info", "debug"]) + .default("info"), + RUN_REPLICATION_LEADER_LOCK_ADDITIONAL_TIME_MS: z.coerce.number().int().default(10_000), + RUN_REPLICATION_LEADER_LOCK_RETRY_INTERVAL_MS: z.coerce.number().int().default(500), + RUN_REPLICATION_WAIT_FOR_ASYNC_INSERT: z.string().default("0"), + RUN_REPLICATION_KEEP_ALIVE_ENABLED: z.string().default("0"), + RUN_REPLICATION_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), + RUN_REPLICATION_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), + // Retry configuration for insert operations + RUN_REPLICATION_INSERT_MAX_RETRIES: z.coerce.number().int().default(3), + RUN_REPLICATION_INSERT_BASE_DELAY_MS: z.coerce.number().int().default(100), + RUN_REPLICATION_INSERT_MAX_DELAY_MS: z.coerce.number().int().default(2000), + RUN_REPLICATION_INSERT_STRATEGY: z.enum(["insert", "insert_async"]).default("insert"), + + // Clickhouse + CLICKHOUSE_URL: z.string(), + CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"), + CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), + CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), + CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"), + + // Bootstrap + TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"), + TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME: z.string().optional(), + TRIGGER_BOOTSTRAP_WORKER_TOKEN_PATH: z.string().optional(), + + // Machine presets + MACHINE_PRESETS_OVERRIDE_PATH: z.string().optional(), + + // CLI package tag (e.g. "latest", "v4-beta", "4.0.0") - used for setup commands + TRIGGER_CLI_TAG: z.string().default("latest"), + + HEALTHCHECK_DATABASE_DISABLED: z.string().default("0"), + + REQUEST_IDEMPOTENCY_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + REQUEST_IDEMPOTENCY_REDIS_READER_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_READER_HOST), + REQUEST_IDEMPOTENCY_REDIS_READER_PORT: z.coerce + .number() + .optional() + .transform( + (v) => + v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) + ), + REQUEST_IDEMPOTENCY_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + REQUEST_IDEMPOTENCY_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + REQUEST_IDEMPOTENCY_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + REQUEST_IDEMPOTENCY_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + REQUEST_IDEMPOTENCY_LOG_LEVEL: z + .enum(["log", "error", "warn", "info", "debug"]) + .default("info"), + + REQUEST_IDEMPOTENCY_TTL_IN_MS: z.coerce + .number() + .int() + .default(60_000 * 60 * 24), + + // Bulk action + BULK_ACTION_BATCH_SIZE: z.coerce.number().int().default(100), + BULK_ACTION_BATCH_DELAY_MS: z.coerce.number().int().default(200), + BULK_ACTION_SUBBATCH_CONCURRENCY: z.coerce.number().int().default(5), + + // AI Run Filter + AI_RUN_FILTER_MODEL: z.string().optional(), + + EVENT_LOOP_MONITOR_THRESHOLD_MS: z.coerce.number().int().default(100), + + VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(), + }) + .and(GithubAppEnvSchema); export type Environment = z.infer; export const env = EnvironmentSchema.parse(process.env); diff --git a/apps/webapp/app/routes/auth.github.ts b/apps/webapp/app/routes/auth.github.ts index ec376153fc..a4adc7f28d 100644 --- a/apps/webapp/app/routes/auth.github.ts +++ b/apps/webapp/app/routes/auth.github.ts @@ -1,6 +1,4 @@ -import type { ActionFunction, LoaderFunction } from "@remix-run/node"; -import { createCookie } from "@remix-run/node"; -import { redirect } from "@remix-run/node"; +import { type ActionFunction, type LoaderFunction, redirect, createCookie } from "@remix-run/node"; import { authenticator } from "~/services/auth.server"; export let loader: LoaderFunction = () => redirect("/login"); diff --git a/apps/webapp/app/services/gitHub.server.ts b/apps/webapp/app/services/gitHub.server.ts index 4224e4e247..6287d882b5 100644 --- a/apps/webapp/app/services/gitHub.server.ts +++ b/apps/webapp/app/services/gitHub.server.ts @@ -2,13 +2,16 @@ import { App, type Octokit } from "octokit"; import { env } from "../env.server"; import { prisma } from "~/db.server"; -export const githubApp = new App({ - appId: env.GITHUB_APP_ID, - privateKey: env.GITHUB_APP_PRIVATE_KEY, - webhooks: { - secret: env.GITHUB_APP_WEBHOOK_SECRET, - }, -}); +export const githubApp = + env.GITHUB_APP_ENABLED === "1" + ? new App({ + appId: env.GITHUB_APP_ID, + privateKey: env.GITHUB_APP_PRIVATE_KEY, + webhooks: { + secret: env.GITHUB_APP_WEBHOOK_SECRET, + }, + }) + : null; /** * Links a GitHub App installation to a Trigger organization @@ -17,6 +20,10 @@ export async function linkGitHubAppInstallation( installationId: number, organizationId: string ): Promise { + if (!githubApp) { + throw new Error("GitHub App is not enabled"); + } + const octokit = await githubApp.getInstallationOctokit(installationId); const { data: installation } = await octokit.rest.apps.getInstallation({ installation_id: installationId, diff --git a/apps/webapp/app/services/gitHubSession.server.ts b/apps/webapp/app/services/gitHubSession.server.ts index 531662529b..499c1dff1a 100644 --- a/apps/webapp/app/services/gitHubSession.server.ts +++ b/apps/webapp/app/services/gitHubSession.server.ts @@ -22,6 +22,10 @@ export async function createGitHubAppInstallSession( organizationId: string, redirectTo: string ): Promise<{ url: string; cookieHeader: string }> { + if (env.GITHUB_APP_ENABLED !== "1") { + throw new Error("GitHub App is not enabled"); + } + const state = randomBytes(32).toString("hex"); const session = await sessionStorage.getSession(); From 114892f3661807b9536e3d7c548477b3f8da18ea Mon Sep 17 00:00:00 2001 From: myftija Date: Fri, 29 Aug 2025 16:22:04 +0200 Subject: [PATCH 04/15] Add additional org check on gh app installation callback --- .../app/routes/_app.github.callback/route.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/webapp/app/routes/_app.github.callback/route.tsx b/apps/webapp/app/routes/_app.github.callback/route.tsx index 6023afbe75..9f86ff4987 100644 --- a/apps/webapp/app/routes/_app.github.callback/route.tsx +++ b/apps/webapp/app/routes/_app.github.callback/route.tsx @@ -5,6 +5,8 @@ import { linkGitHubAppInstallation } from "~/services/gitHub.server"; import { logger } from "~/services/logger.server"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { tryCatch } from "@trigger.dev/core"; +import { $replica } from "~/db.server"; +import { requireUser } from "~/services/session.server"; const QuerySchema = z.object({ installation_id: z.coerce.number(), @@ -42,6 +44,25 @@ export async function loader({ request }: LoaderFunctionArgs) { const { organizationId, redirectTo } = sessionResult; + const user = await requireUser(request); + const org = await $replica.organization.findFirst({ + where: { id: organizationId, members: { some: { userId: user.id } }, deletedAt: null }, + orderBy: { createdAt: "desc" }, + select: { + id: true, + }, + }); + + if (!org) { + // the secure cookie approach should already protect against this + // just an additional check + logger.error("GitHub app installation attempt on unauthenticated org", { + userId: user.id, + organizationId, + }); + return redirectWithErrorMessage(redirectTo, request, "Failed to install GitHub App"); + } + switch (setup_action) { case "install": case "update": { From 6b311cc103a0642dda5863f45248c80e2e9574f9 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 1 Sep 2025 13:29:12 +0200 Subject: [PATCH 05/15] Save account handle and repo default branch on install --- apps/webapp/app/services/gitHub.server.ts | 8 +++ .../migration.sql | 59 +------------------ .../database/prisma/schema.prisma | 12 ++-- 3 files changed, 17 insertions(+), 62 deletions(-) diff --git a/apps/webapp/app/services/gitHub.server.ts b/apps/webapp/app/services/gitHub.server.ts index 6287d882b5..2e80bb286b 100644 --- a/apps/webapp/app/services/gitHub.server.ts +++ b/apps/webapp/app/services/gitHub.server.ts @@ -39,6 +39,13 @@ export async function linkGitHubAppInstallation( organizationId, targetId: installation.target_id, targetType: installation.target_type, + accountHandle: installation.account + ? "login" in installation.account + ? installation.account.login + : "slug" in installation.account + ? installation.account.slug + : "-" + : "-", permissions: installation.permissions, repositorySelection, repositories: { @@ -76,5 +83,6 @@ async function fetchInstallationRepositories(octokit: Octokit, installationId: n fullName: repo.full_name, htmlUrl: repo.html_url, private: repo.private, + defaultBranch: repo.default_branch, })); } diff --git a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql index 0be09fcb1c..ce39614f93 100644 --- a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql +++ b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql @@ -1,51 +1,11 @@ --- CreateEnum CREATE TYPE "public"."GithubRepositorySelection" AS ENUM ('ALL', 'SELECTED'); --- DropIndex -DROP INDEX "public"."SecretStore_key_idx"; - --- DropIndex -DROP INDEX "public"."TaskRun_runtimeEnvironmentId_createdAt_idx"; - --- DropIndex -DROP INDEX "public"."TaskRun_runtimeEnvironmentId_id_idx"; - --- AlterTable -ALTER TABLE "public"."_BackgroundWorkerToBackgroundWorkerFile" ADD CONSTRAINT "_BackgroundWorkerToBackgroundWorkerFile_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "public"."_BackgroundWorkerToBackgroundWorkerFile_AB_unique"; - --- AlterTable -ALTER TABLE "public"."_BackgroundWorkerToTaskQueue" ADD CONSTRAINT "_BackgroundWorkerToTaskQueue_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "public"."_BackgroundWorkerToTaskQueue_AB_unique"; - --- AlterTable -ALTER TABLE "public"."_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "public"."_TaskRunToTaskRunTag_AB_unique"; - --- AlterTable -ALTER TABLE "public"."_WaitpointRunConnections" ADD CONSTRAINT "_WaitpointRunConnections_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "public"."_WaitpointRunConnections_AB_unique"; - --- AlterTable -ALTER TABLE "public"."_completedWaitpoints" ADD CONSTRAINT "_completedWaitpoints_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "public"."_completedWaitpoints_AB_unique"; - --- CreateTable CREATE TABLE "public"."GithubAppInstallation" ( "id" TEXT NOT NULL, "appInstallationId" INTEGER NOT NULL, "targetId" INTEGER NOT NULL, "targetType" TEXT NOT NULL, + "accountHandle" TEXT NOT NULL, "permissions" JSONB, "repositorySelection" "public"."GithubRepositorySelection" NOT NULL, "installedBy" TEXT, @@ -58,7 +18,6 @@ CREATE TABLE "public"."GithubAppInstallation" ( CONSTRAINT "GithubAppInstallation_pkey" PRIMARY KEY ("id") ); --- CreateTable CREATE TABLE "public"."GithubRepository" ( "id" TEXT NOT NULL, "githubId" INTEGER NOT NULL, @@ -66,6 +25,7 @@ CREATE TABLE "public"."GithubRepository" ( "fullName" TEXT NOT NULL, "htmlUrl" TEXT NOT NULL, "private" BOOLEAN NOT NULL, + "defaultBranch" TEXT NOT NULL, "removedAt" TIMESTAMP(3), "installationId" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -74,29 +34,14 @@ CREATE TABLE "public"."GithubRepository" ( CONSTRAINT "GithubRepository_pkey" PRIMARY KEY ("id") ); --- CreateIndex CREATE UNIQUE INDEX "GithubAppInstallation_appInstallationId_key" ON "public"."GithubAppInstallation"("appInstallationId"); --- CreateIndex CREATE INDEX "GithubAppInstallation_organizationId_idx" ON "public"."GithubAppInstallation"("organizationId"); --- CreateIndex CREATE INDEX "GithubRepository_installationId_idx" ON "public"."GithubRepository"("installationId"); --- CreateIndex CREATE UNIQUE INDEX "GithubRepository_installationId_githubId_key" ON "public"."GithubRepository"("installationId", "githubId"); --- CreateIndex -CREATE INDEX "SecretStore_key_idx" ON "public"."SecretStore"("key" text_pattern_ops); - --- CreateIndex -CREATE INDEX "TaskRun_runtimeEnvironmentId_id_idx" ON "public"."TaskRun"("runtimeEnvironmentId", "id" DESC); - --- CreateIndex -CREATE INDEX "TaskRun_runtimeEnvironmentId_createdAt_idx" ON "public"."TaskRun"("runtimeEnvironmentId", "createdAt" DESC); - --- AddForeignKey ALTER TABLE "public"."GithubAppInstallation" ADD CONSTRAINT "GithubAppInstallation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; --- AddForeignKey ALTER TABLE "public"."GithubRepository" ADD CONSTRAINT "GithubRepository_installationId_fkey" FOREIGN KEY ("installationId") REFERENCES "public"."GithubAppInstallation"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 9acd6d4264..90b3fe53d5 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -2251,6 +2251,7 @@ model GithubAppInstallation { appInstallationId Int @unique targetId Int targetType String + accountHandle String permissions Json? repositorySelection GithubRepositorySelection installedBy String? @@ -2271,11 +2272,12 @@ model GithubAppInstallation { model GithubRepository { id String @id @default(cuid()) - githubId Int - name String - fullName String - htmlUrl String - private Boolean + githubId Int + name String + fullName String + htmlUrl String + private Boolean + defaultBranch String removedAt DateTime? From ec03a4c97d193505972f9275bbe462e0cb0e4b51 Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 09:48:47 +0200 Subject: [PATCH 06/15] Do repo hard deletes in favor of simplicity --- .../20250829124138_add_gh_installation_schema/migration.sql | 1 - internal-packages/database/prisma/schema.prisma | 2 -- 2 files changed, 3 deletions(-) diff --git a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql index ce39614f93..e39a37d977 100644 --- a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql +++ b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql @@ -26,7 +26,6 @@ CREATE TABLE "public"."GithubRepository" ( "htmlUrl" TEXT NOT NULL, "private" BOOLEAN NOT NULL, "defaultBranch" TEXT NOT NULL, - "removedAt" TIMESTAMP(3), "installationId" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 90b3fe53d5..fea27abe68 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -2279,8 +2279,6 @@ model GithubRepository { private Boolean defaultBranch String - removedAt DateTime? - installation GithubAppInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade, onUpdate: Cascade) installationId String From a20d97bee15b054214cb8e983945e8fe522fbffe Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 09:55:29 +0200 Subject: [PATCH 07/15] Disable github app by default --- apps/webapp/app/env.server.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index e30022526b..696fcf01ca 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -3,18 +3,22 @@ import { BoolEnv } from "./utils/boolEnv"; import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; -const GithubAppEnvSchema = z.union([ - z.object({ - GITHUB_APP_ENABLED: z.literal("1"), - GITHUB_APP_ID: z.string(), - GITHUB_APP_PRIVATE_KEY: z.string(), - GITHUB_APP_WEBHOOK_SECRET: z.string(), - GITHUB_APP_SLUG: z.string(), - }), - z.object({ - GITHUB_APP_ENABLED: z.literal("0"), - }), -]); +const GithubAppEnvSchema = z + .discriminatedUnion("GITHUB_APP_ENABLED", [ + z.object({ + GITHUB_APP_ENABLED: z.literal("1"), + GITHUB_APP_ID: z.string(), + GITHUB_APP_PRIVATE_KEY: z.string(), + GITHUB_APP_WEBHOOK_SECRET: z.string(), + GITHUB_APP_SLUG: z.string(), + }), + z.object({ + GITHUB_APP_ENABLED: z.literal("0"), + }), + ]) + .default({ + GITHUB_APP_ENABLED: "0", + }); const EnvironmentSchema = z .object({ From b2c20e27ff81a4280cf270a490c7f89ac3a00ceb Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 12:55:26 +0200 Subject: [PATCH 08/15] Fix gh env schema union issue --- apps/webapp/app/env.server.ts | 36 ++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 696fcf01ca..2d6b3dbdac 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -4,21 +4,27 @@ import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; const GithubAppEnvSchema = z - .discriminatedUnion("GITHUB_APP_ENABLED", [ - z.object({ - GITHUB_APP_ENABLED: z.literal("1"), - GITHUB_APP_ID: z.string(), - GITHUB_APP_PRIVATE_KEY: z.string(), - GITHUB_APP_WEBHOOK_SECRET: z.string(), - GITHUB_APP_SLUG: z.string(), - }), - z.object({ - GITHUB_APP_ENABLED: z.literal("0"), - }), - ]) - .default({ - GITHUB_APP_ENABLED: "0", - }); + .preprocess( + (val) => { + const obj = val as any; + if (!obj || !obj.GITHUB_APP_ENABLED) { + return { ...obj, GITHUB_APP_ENABLED: "0" }; + } + return obj; + }, + z.discriminatedUnion("GITHUB_APP_ENABLED", [ + z.object({ + GITHUB_APP_ENABLED: z.literal("1"), + GITHUB_APP_ID: z.string(), + GITHUB_APP_PRIVATE_KEY: z.string(), + GITHUB_APP_WEBHOOK_SECRET: z.string(), + GITHUB_APP_SLUG: z.string(), + }), + z.object({ + GITHUB_APP_ENABLED: z.literal("0"), + }), + ]) + ); const EnvironmentSchema = z .object({ From 417f239000d59c17b9f90c50b42e3af340de1434 Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 13:15:35 +0200 Subject: [PATCH 09/15] Use octokit's iterator for paginating repos --- apps/webapp/app/services/gitHub.server.ts | 33 ++++++++++++----------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/webapp/app/services/gitHub.server.ts b/apps/webapp/app/services/gitHub.server.ts index 2e80bb286b..c72b7d243b 100644 --- a/apps/webapp/app/services/gitHub.server.ts +++ b/apps/webapp/app/services/gitHub.server.ts @@ -1,6 +1,7 @@ import { App, type Octokit } from "octokit"; import { env } from "../env.server"; import { prisma } from "~/db.server"; +import { logger } from "./logger.server"; export const githubApp = env.GITHUB_APP_ENABLED === "1" @@ -56,28 +57,30 @@ export async function linkGitHubAppInstallation( } async function fetchInstallationRepositories(octokit: Octokit, installationId: number) { - const all = []; - let page = 1; - const perPage = 100; - const maxPages = 3; + const iterator = octokit.paginate.iterator(octokit.rest.apps.listReposAccessibleToInstallation, { + installation_id: installationId, + per_page: 100, + }); - while (page <= maxPages) { - const { data: repoData } = await octokit.rest.apps.listReposAccessibleToInstallation({ - installation_id: installationId, - per_page: perPage, - page, - }); + const allRepos = []; + const maxPages = 3; + let pageCount = 0; - all.push(...repoData.repositories); + for await (const { data } of iterator) { + pageCount++; + allRepos.push(...data); - if (repoData.repositories.length < perPage) { + if (maxPages && pageCount >= maxPages) { + logger.warn("GitHub installation repository fetch truncated", { + installationId, + maxPages, + totalReposFetched: allRepos.length, + }); break; } - - page++; } - return all.map((repo) => ({ + return allRepos.map((repo) => ({ githubId: repo.id, name: repo.name, fullName: repo.full_name, From aa51adf6ac96476f599b56d8dda880222d6dd6c5 Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 13:24:51 +0200 Subject: [PATCH 10/15] Parse gh app install callback with a discriminated union --- .../app/routes/_app.github.callback/route.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/routes/_app.github.callback/route.tsx b/apps/webapp/app/routes/_app.github.callback/route.tsx index 9f86ff4987..e9c25173f7 100644 --- a/apps/webapp/app/routes/_app.github.callback/route.tsx +++ b/apps/webapp/app/routes/_app.github.callback/route.tsx @@ -8,11 +8,22 @@ import { tryCatch } from "@trigger.dev/core"; import { $replica } from "~/db.server"; import { requireUser } from "~/services/session.server"; -const QuerySchema = z.object({ - installation_id: z.coerce.number(), - setup_action: z.enum(["install", "update", "request"]), - state: z.string(), -}); +const QuerySchema = z.discriminatedUnion("setup_action", [ + z.object({ + setup_action: z.literal("install"), + installation_id: z.coerce.number(), + state: z.string(), + }), + z.object({ + setup_action: z.literal("update"), + installation_id: z.coerce.number(), + state: z.string(), + }), + z.object({ + setup_action: z.literal("request"), + state: z.string(), + }), +]); export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); @@ -28,14 +39,13 @@ export async function loader({ request }: LoaderFunctionArgs) { return redirectWithErrorMessage("/", request, "Failed to install GitHub App"); } - const { installation_id, setup_action, state } = result.data; + const callbackData = result.data; - const sessionResult = await validateGitHubAppInstallSession(cookieHeader, state); + const sessionResult = await validateGitHubAppInstallSession(cookieHeader, callbackData.state); if (!sessionResult.valid) { logger.error("GitHub App callback with invalid session", { - state, - installation_id, + callbackData, error: sessionResult.error, }); @@ -63,10 +73,12 @@ export async function loader({ request }: LoaderFunctionArgs) { return redirectWithErrorMessage(redirectTo, request, "Failed to install GitHub App"); } - switch (setup_action) { + switch (callbackData.setup_action) { case "install": case "update": { - const [error] = await tryCatch(linkGitHubAppInstallation(installation_id, organizationId)); + const [error] = await tryCatch( + linkGitHubAppInstallation(callbackData.installation_id, organizationId) + ); if (error) { logger.error("Failed to link GitHub App installation", { @@ -82,14 +94,14 @@ export async function loader({ request }: LoaderFunctionArgs) { // This happens when a non-admin user requests installation // The installation_id won't be available until an admin approves logger.info("GitHub App installation requested, awaiting approval", { - state, + callbackData, }); return redirectWithSuccessMessage(redirectTo, request, "GitHub App installation requested"); } default: - setup_action satisfies never; + callbackData satisfies never; return redirectWithErrorMessage(redirectTo, request, "Failed to install GitHub App"); } } From 6d375af9e31a98623837f554fc4a7c707d44aeea Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 13:35:17 +0200 Subject: [PATCH 11/15] Remove duplicate env vars --- apps/webapp/app/env.server.ts | 49 +++++++++++++++-------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 2d6b3dbdac..350463a5b0 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -3,28 +3,27 @@ import { BoolEnv } from "./utils/boolEnv"; import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; -const GithubAppEnvSchema = z - .preprocess( - (val) => { - const obj = val as any; - if (!obj || !obj.GITHUB_APP_ENABLED) { - return { ...obj, GITHUB_APP_ENABLED: "0" }; - } - return obj; - }, - z.discriminatedUnion("GITHUB_APP_ENABLED", [ - z.object({ - GITHUB_APP_ENABLED: z.literal("1"), - GITHUB_APP_ID: z.string(), - GITHUB_APP_PRIVATE_KEY: z.string(), - GITHUB_APP_WEBHOOK_SECRET: z.string(), - GITHUB_APP_SLUG: z.string(), - }), - z.object({ - GITHUB_APP_ENABLED: z.literal("0"), - }), - ]) - ); +const GithubAppEnvSchema = z.preprocess( + (val) => { + const obj = val as any; + if (!obj || !obj.GITHUB_APP_ENABLED) { + return { ...obj, GITHUB_APP_ENABLED: "0" }; + } + return obj; + }, + z.discriminatedUnion("GITHUB_APP_ENABLED", [ + z.object({ + GITHUB_APP_ENABLED: z.literal("1"), + GITHUB_APP_ID: z.string(), + GITHUB_APP_PRIVATE_KEY: z.string(), + GITHUB_APP_WEBHOOK_SECRET: z.string(), + GITHUB_APP_SLUG: z.string(), + }), + z.object({ + GITHUB_APP_ENABLED: z.literal("0"), + }), + ]) +); const EnvironmentSchema = z .object({ @@ -83,12 +82,6 @@ const EnvironmentSchema = z SMTP_USER: z.string().optional(), SMTP_PASSWORD: z.string().optional(), - // GitHub App - GITHUB_APP_ID: z.string(), - GITHUB_APP_PRIVATE_KEY: z.string(), - GITHUB_APP_WEBHOOK_SECRET: z.string(), - GITHUB_APP_SLUG: z.string(), - PLAIN_API_KEY: z.string().optional(), WORKER_SCHEMA: z.string().default("graphile_worker"), WORKER_CONCURRENCY: z.coerce.number().int().default(10), From 21529b4fd549085580f62a02c4b5c976d94ebfbc Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 15:01:31 +0200 Subject: [PATCH 12/15] Use bigint for github integer IDs --- .../20250829124138_add_gh_installation_schema/migration.sql | 6 +++--- internal-packages/database/prisma/schema.prisma | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql index e39a37d977..78b5e6bf2c 100644 --- a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql +++ b/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql @@ -2,8 +2,8 @@ CREATE TYPE "public"."GithubRepositorySelection" AS ENUM ('ALL', 'SELECTED'); CREATE TABLE "public"."GithubAppInstallation" ( "id" TEXT NOT NULL, - "appInstallationId" INTEGER NOT NULL, - "targetId" INTEGER NOT NULL, + "appInstallationId" BIGINT NOT NULL, + "targetId" BIGINT NOT NULL, "targetType" TEXT NOT NULL, "accountHandle" TEXT NOT NULL, "permissions" JSONB, @@ -20,7 +20,7 @@ CREATE TABLE "public"."GithubAppInstallation" ( CREATE TABLE "public"."GithubRepository" ( "id" TEXT NOT NULL, - "githubId" INTEGER NOT NULL, + "githubId" BIGINT NOT NULL, "name" TEXT NOT NULL, "fullName" TEXT NOT NULL, "htmlUrl" TEXT NOT NULL, diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index fea27abe68..edcc82f535 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -2248,8 +2248,8 @@ enum GithubRepositorySelection { model GithubAppInstallation { id String @id @default(cuid()) - appInstallationId Int @unique - targetId Int + appInstallationId BigInt @unique + targetId BigInt targetType String accountHandle String permissions Json? @@ -2272,7 +2272,7 @@ model GithubAppInstallation { model GithubRepository { id String @id @default(cuid()) - githubId Int + githubId BigInt name String fullName String htmlUrl String From 1d931c895fd7230ff998996dd91716d285ad137d Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 15:23:32 +0200 Subject: [PATCH 13/15] Sanitize redirect paths in the gh installation and auth flow --- .../app/routes/_app.github.callback/route.tsx | 4 ++- .../app/routes/_app.github.install/route.tsx | 5 +++- .../app/routes/auth.github.callback.tsx | 3 +- apps/webapp/app/utils.ts | 30 ++++++++++++++----- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/routes/_app.github.callback/route.tsx b/apps/webapp/app/routes/_app.github.callback/route.tsx index e9c25173f7..e1b1508797 100644 --- a/apps/webapp/app/routes/_app.github.callback/route.tsx +++ b/apps/webapp/app/routes/_app.github.callback/route.tsx @@ -7,6 +7,7 @@ import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/m import { tryCatch } from "@trigger.dev/core"; import { $replica } from "~/db.server"; import { requireUser } from "~/services/session.server"; +import { sanitizeRedirectPath } from "~/utils"; const QuerySchema = z.discriminatedUnion("setup_action", [ z.object({ @@ -52,7 +53,8 @@ export async function loader({ request }: LoaderFunctionArgs) { return redirectWithErrorMessage("/", request, "Failed to install GitHub App"); } - const { organizationId, redirectTo } = sessionResult; + const { organizationId, redirectTo: unsafeRedirectTo } = sessionResult; + const redirectTo = sanitizeRedirectPath(unsafeRedirectTo); const user = await requireUser(request); const org = await $replica.organization.findFirst({ diff --git a/apps/webapp/app/routes/_app.github.install/route.tsx b/apps/webapp/app/routes/_app.github.install/route.tsx index b47fbbbd32..42d68e5bec 100644 --- a/apps/webapp/app/routes/_app.github.install/route.tsx +++ b/apps/webapp/app/routes/_app.github.install/route.tsx @@ -6,10 +6,13 @@ import { createGitHubAppInstallSession } from "~/services/gitHubSession.server"; import { requireUser } from "~/services/session.server"; import { newOrganizationPath } from "~/utils/pathBuilder"; import { logger } from "~/services/logger.server"; +import { sanitizeRedirectPath } from "~/utils"; const QuerySchema = z.object({ org_slug: z.string(), - redirect_to: z.string(), + redirect_to: z.string().refine((value) => value === sanitizeRedirectPath(value), { + message: "Invalid redirect path", + }), }); export const loader = async ({ request }: LoaderFunctionArgs) => { diff --git a/apps/webapp/app/routes/auth.github.callback.tsx b/apps/webapp/app/routes/auth.github.callback.tsx index d85a68a472..ee8776ae81 100644 --- a/apps/webapp/app/routes/auth.github.callback.tsx +++ b/apps/webapp/app/routes/auth.github.callback.tsx @@ -5,11 +5,12 @@ import { getSession, redirectWithErrorMessage } from "~/models/message.server"; import { authenticator } from "~/services/auth.server"; import { commitSession } from "~/services/sessionStorage.server"; import { redirectCookie } from "./auth.github"; +import { sanitizeRedirectPath } from "~/utils"; export let loader: LoaderFunction = async ({ request }) => { const cookie = request.headers.get("Cookie"); const redirectValue = await redirectCookie.parse(cookie); - const redirectTo = redirectValue ?? "/"; + const redirectTo = sanitizeRedirectPath(redirectValue); const auth = await authenticator.authenticate("github", request, { failureRedirect: "/login", // If auth fails, the failureRedirect will be thrown as a Response diff --git a/apps/webapp/app/utils.ts b/apps/webapp/app/utils.ts index 7dc407cec6..adb1829281 100644 --- a/apps/webapp/app/utils.ts +++ b/apps/webapp/app/utils.ts @@ -7,22 +7,38 @@ const DEFAULT_REDIRECT = "/"; * This should be used any time the redirect path is user-provided * (Like the query string on our login/signup pages). This avoids * open-redirect vulnerabilities. - * @param {string} to The redirect destination + * @param {string} path The redirect destination * @param {string} defaultRedirect The redirect to use if the to is unsafe. */ -export function safeRedirect( - to: FormDataEntryValue | string | null | undefined, +export function sanitizeRedirectPath( + path: string | undefined | null, defaultRedirect: string = DEFAULT_REDIRECT -) { - if (!to || typeof to !== "string") { +): string { + if (!path || typeof path !== "string") { return defaultRedirect; } - if (!to.startsWith("/") || to.startsWith("//")) { + if (!path.startsWith("/") || path.startsWith("//")) { return defaultRedirect; } - return to; + try { + // should not parse as a full URL + new URL(path); + return defaultRedirect; + } catch {} + + try { + // ensure it's a valid relative path + const url = new URL(path, "https://example.com"); + if (url.hostname !== "example.com") { + return defaultRedirect; + } + } catch { + return defaultRedirect; + } + + return path; } /** From d07e60543c84e91dd4c71046def76ce2ed6bd2fd Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 15:36:31 +0200 Subject: [PATCH 14/15] Regenerate migration after rebase on main to fix ordering --- .../migration.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal-packages/database/prisma/migrations/{20250829124138_add_gh_installation_schema => 20250902133449_add_gh_installation_schema}/migration.sql (100%) diff --git a/internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql b/internal-packages/database/prisma/migrations/20250902133449_add_gh_installation_schema/migration.sql similarity index 100% rename from internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql rename to internal-packages/database/prisma/migrations/20250902133449_add_gh_installation_schema/migration.sql From 9a7eb495ccde05f556362931e42c6ea4a43d9e37 Mon Sep 17 00:00:00 2001 From: myftija Date: Tue, 2 Sep 2025 16:04:23 +0200 Subject: [PATCH 15/15] Handle gh install updates separately from new installs --- .../app/routes/_app.github.callback/route.tsx | 18 ++++++-- apps/webapp/app/services/gitHub.server.ts | 44 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/_app.github.callback/route.tsx b/apps/webapp/app/routes/_app.github.callback/route.tsx index e1b1508797..44c7f37c13 100644 --- a/apps/webapp/app/routes/_app.github.callback/route.tsx +++ b/apps/webapp/app/routes/_app.github.callback/route.tsx @@ -1,7 +1,7 @@ import { type LoaderFunctionArgs } from "@remix-run/node"; import { z } from "zod"; import { validateGitHubAppInstallSession } from "~/services/gitHubSession.server"; -import { linkGitHubAppInstallation } from "~/services/gitHub.server"; +import { linkGitHubAppInstallation, updateGitHubAppInstallation } from "~/services/gitHub.server"; import { logger } from "~/services/logger.server"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { tryCatch } from "@trigger.dev/core"; @@ -76,8 +76,7 @@ export async function loader({ request }: LoaderFunctionArgs) { } switch (callbackData.setup_action) { - case "install": - case "update": { + case "install": { const [error] = await tryCatch( linkGitHubAppInstallation(callbackData.installation_id, organizationId) ); @@ -92,6 +91,19 @@ export async function loader({ request }: LoaderFunctionArgs) { return redirectWithSuccessMessage(redirectTo, request, "GitHub App installed successfully"); } + case "update": { + const [error] = await tryCatch(updateGitHubAppInstallation(callbackData.installation_id)); + + if (error) { + logger.error("Failed to update GitHub App installation", { + error, + }); + return redirectWithErrorMessage(redirectTo, request, "Failed to update GitHub App"); + } + + return redirectWithSuccessMessage(redirectTo, request, "GitHub App updated successfully"); + } + case "request": { // This happens when a non-admin user requests installation // The installation_id won't be available until an admin approves diff --git a/apps/webapp/app/services/gitHub.server.ts b/apps/webapp/app/services/gitHub.server.ts index c72b7d243b..9e4c26a554 100644 --- a/apps/webapp/app/services/gitHub.server.ts +++ b/apps/webapp/app/services/gitHub.server.ts @@ -56,6 +56,50 @@ export async function linkGitHubAppInstallation( }); } +/** + * Links a GitHub App installation to a Trigger organization + */ +export async function updateGitHubAppInstallation(installationId: number): Promise { + if (!githubApp) { + throw new Error("GitHub App is not enabled"); + } + + const octokit = await githubApp.getInstallationOctokit(installationId); + const { data: installation } = await octokit.rest.apps.getInstallation({ + installation_id: installationId, + }); + + const existingInstallation = await prisma.githubAppInstallation.findFirst({ + where: { appInstallationId: installationId }, + }); + + if (!existingInstallation) { + throw new Error("GitHub App installation not found"); + } + + const repositorySelection = installation.repository_selection === "all" ? "ALL" : "SELECTED"; + + // repos are updated asynchronously via webhook events + await prisma.githubAppInstallation.update({ + where: { id: existingInstallation?.id }, + data: { + appInstallationId: installationId, + targetId: installation.target_id, + targetType: installation.target_type, + accountHandle: installation.account + ? "login" in installation.account + ? installation.account.login + : "slug" in installation.account + ? installation.account.slug + : "-" + : "-", + permissions: installation.permissions, + suspendedAt: existingInstallation?.suspendedAt, + repositorySelection, + }, + }); +} + async function fetchInstallationRepositories(octokit: Octokit, installationId: number) { const iterator = octokit.paginate.iterator(octokit.rest.apps.listReposAccessibleToInstallation, { installation_id: installationId,