import { MachinePresetName, tryCatch } from "@trigger.dev/core/v3"; import type { RuntimeEnvironmentType } from "@trigger.dev/database"; import { BillingClient, defaultMachine as defaultMachineFromPlatform, machines as machinesFromPlatform, type BillingAlertsResult, type CreatePrivateLinkConnectionBody, type Limits, type MachineCode, type PrivateLinkConnection, type PrivateLinkConnectionList, type PrivateLinkRegionsResult, type SetPlanBody, type UpdateBillingAlertsRequest, type UsageResult, type UsageSeriesParams, type CurrentPlan, } from "@trigger.dev/platform"; import { BillingLimitResultSchema, BillingLimitsActiveResultSchema, BillingLimitsPendingResolvesResultSchema, EntitlementResultSchema, asPlatformSchema, type BillingLimitResult, type BillingLimitsActiveResult, type BillingLimitsPendingResolvesResult, type EntitlementResult, type ResolveBillingLimitRequest, type UpdateBillingLimitRequest, } from "~/services/billingLimit.schemas"; import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { createLRUMemoryStore } from "@internal/cache"; import { existsSync, readFileSync } from "node:fs"; import { redirect } from "remix-typedjson"; import { z } from "zod"; import { env } from "~/env.server"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { logger } from "~/services/logger.server"; import { newProjectPath, organizationBillingPath } from "~/utils/pathBuilder"; import { singleton } from "~/utils/singleton"; import { RedisCacheStore } from "./unkey/redisCacheStore.server"; import { $replica } from "~/db.server"; import { metrics } from "@opentelemetry/api"; function initializeClient() { if (isCloud() && process.env.BILLING_API_URL && process.env.BILLING_API_KEY) { const client = new BillingClient({ url: process.env.BILLING_API_URL, apiKey: process.env.BILLING_API_KEY, }); return client; } } const client = singleton("billingClient", initializeClient); /** * `true` when the billing client was instantiated — i.e. we're running * in a cloud-style install with `BILLING_API_URL` + `BILLING_API_KEY` * configured. OSS / self-hosted installs return `false` here, which * lets callers distinguish "no billing wired up, fall back to * defaults" from "billing wired up but the call failed, retry." */ export function isBillingConfigured(): boolean { return client !== undefined; } // Failures from @trigger.dev/platform billing client calls are tracked via // this metric (with low-cardinality {function, kind} labels) rather than // logged. Every task invocation hits these paths, so per-call logs were too // noisy; dashboard the counter for visibility instead. const platformClientMeter = metrics.getMeter("trigger.dev/platform-client"); const platformClientFailuresCounter = platformClientMeter.createCounter( "platform_client.failures_total", { description: "Failures returned or thrown by @trigger.dev/platform billing client calls", } ); function recordPlatformFailure(fn: string, kind: "caught" | "no_success") { platformClientFailuresCounter.add(1, { function: fn, kind }); } export type ValidatedPromoCode = { valid: boolean; amountInCents?: number; expiresAt?: string | null; }; /** * Validate a promo code (no org context). Returns `undefined` when billing * isn't configured or the call fails, so callers fall back to treating the * code as not-yet-validated rather than crashing the page. */ export async function validatePromoCode(code: string): Promise { if (!client) { return undefined; } const [error, result] = await tryCatch(client.validatePromoCode(code)); if (error) { recordPlatformFailure("validatePromoCode", "caught"); logger.error("validatePromoCode threw", { error }); return undefined; } if (!result.success) { recordPlatformFailure("validatePromoCode", "no_success"); return undefined; } return { valid: result.valid, amountInCents: result.amountInCents, expiresAt: result.expiresAt, }; } export type AppliedPromoCode = { applied: boolean; amountInCents?: number; reason?: string; }; /** * Apply a promo code to a newly created org. Returns `undefined` when billing * isn't configured or the call fails — callers treat that as "not applied" and * must never block org creation on it. */ export async function applyPromoCode( orgId: string, userId: string, code: string ): Promise { if (!client) { return undefined; } const [error, result] = await tryCatch(client.applyPromoCode(orgId, { code, userId })); if (error) { recordPlatformFailure("applyPromoCode", "caught"); logger.error("applyPromoCode threw", { error }); return undefined; } if (!result.success) { recordPlatformFailure("applyPromoCode", "no_success"); return undefined; } return { applied: result.applied, amountInCents: result.amountInCents, reason: result.reason, }; } function initializePlatformCache() { const ctx = new DefaultStatefulContext(); const memory = createLRUMemoryStore(1000); const redisCacheStore = new RedisCacheStore({ connection: { keyPrefix: "tr:cache:platform:v3", port: env.CACHE_REDIS_PORT, host: env.CACHE_REDIS_HOST, username: env.CACHE_REDIS_USERNAME, password: env.CACHE_REDIS_PASSWORD, tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true", clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1", }, }); // This cache holds the limits fetched from the platform service const cache = createCache({ limits: new Namespace(ctx, { stores: [memory, redisCacheStore], fresh: 60_000 * 5, // 5 minutes stale: 60_000 * 10, // 10 minutes }), usage: new Namespace(ctx, { stores: [memory, redisCacheStore], fresh: 60_000 * 5, // 5 minutes stale: 60_000 * 10, // 10 minutes }), entitlement: new Namespace(ctx, { stores: [memory, redisCacheStore], fresh: 60_000, // serve without revalidation for 60s stale: 120_000, // total TTL — fresh 0-60s, stale-revalidate 60-120s }), billingLimit: new Namespace(ctx, { stores: [memory, redisCacheStore], fresh: 60_000, stale: 120_000, }), promoCredits: new Namespace(ctx, { stores: [memory, redisCacheStore], fresh: 60_000, stale: 120_000, }), }); return cache; } const platformCache = singleton("platformCache", initializePlatformCache); function invalidateBillingLimitCaches(organizationId: string) { platformCache.billingLimit.remove(organizationId).catch(() => {}); platformCache.entitlement.remove(organizationId).catch(() => {}); } export function bustBillingLimitCaches(organizationId: string) { invalidateBillingLimitCaches(organizationId); } // Clear the cached promo-credits read so a just-granted code shows on the usage // page immediately rather than after the stale TTL. export function bustPromoCreditsCache(organizationId: string) { platformCache.promoCredits.remove(organizationId).catch(() => {}); } type Machines = typeof machinesFromPlatform; const MachineOverrideValues = z.object({ cpu: z.number(), memory: z.number(), }); type MachineOverrideValues = z.infer; const MachineOverrides = z.record(MachinePresetName, MachineOverrideValues.partial()); type MachineOverrides = z.infer; const MachinePresetOverrides = z.object({ defaultMachine: MachinePresetName.optional(), machines: MachineOverrides.optional(), }); function initializeMachinePresets(): { defaultMachine: MachineCode; machines: Machines; } { const overrides = getMachinePresetOverrides(); if (!overrides) { return { defaultMachine: defaultMachineFromPlatform, machines: machinesFromPlatform, }; } logger.info("🎛️ Overriding machine presets", { overrides }); return { defaultMachine: overrideDefaultMachine(defaultMachineFromPlatform, overrides.defaultMachine), machines: overrideMachines(machinesFromPlatform, overrides.machines), }; } export const { defaultMachine, machines } = singleton("machinePresets", initializeMachinePresets); function overrideDefaultMachine(defaultMachine: MachineCode, override?: MachineCode): MachineCode { if (!override) { return defaultMachine; } return override; } function overrideMachines(machines: Machines, overrides?: MachineOverrides): Machines { if (!overrides) { return machines; } const mergedMachines = { ...machines, }; for (const machine of Object.keys(overrides) as MachinePresetName[]) { mergedMachines[machine] = { ...mergedMachines[machine], ...overrides[machine], }; } return mergedMachines; } function getMachinePresetOverrides() { const path = env.MACHINE_PRESETS_OVERRIDE_PATH; if (!path) { return; } const overrides = safeReadMachinePresetOverrides(path); if (!overrides) { return; } const parsed = MachinePresetOverrides.safeParse(overrides); if (!parsed.success) { logger.error("Error parsing machine preset overrides", { path, error: parsed.error }); return; } return parsed.data; } function safeReadMachinePresetOverrides(path: string) { try { const fileExists = existsSync(path); if (!fileExists) { logger.error("Machine preset overrides file does not exist", { path }); return; } const fileContents = readFileSync(path, "utf8"); return JSON.parse(fileContents); } catch (error) { logger.error("Error reading machine preset overrides", { path, error: error instanceof Error ? error.message : String(error), }); return; } } export async function getCurrentPlan(orgId: string) { if (!client) return undefined; try { const result = await client.currentPlan(orgId); const firstDayOfMonth = new Date(); firstDayOfMonth.setUTCDate(1); firstDayOfMonth.setUTCHours(0, 0, 0, 0); const firstDayOfNextMonth = new Date(); firstDayOfNextMonth.setUTCDate(1); firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1); firstDayOfNextMonth.setUTCHours(0, 0, 0, 0); if (!result.success) { recordPlatformFailure("getCurrentPlan", "no_success"); return undefined; } const periodStart = firstDayOfMonth; const periodEnd = firstDayOfNextMonth; const periodRemainingDuration = periodEnd.getTime() - new Date().getTime(); const usage = { periodStart, periodEnd, periodRemainingDuration, }; return { ...result, usage }; } catch (_e) { recordPlatformFailure("getCurrentPlan", "caught"); return undefined; } } export type SelfServePurchaseBlockReason = "plan_unavailable" | "managed_billing"; /** * When cloud billing is configured, self-serve purchase endpoints must fail closed * if the current plan can't be loaded or the org is on managed billing. */ export function getSelfServePurchaseBlockReason( currentPlan: Awaited> ): SelfServePurchaseBlockReason | undefined { if (!isBillingConfigured()) { return undefined; } if (!currentPlan) { return "plan_unavailable"; } if (currentPlan.v3Subscription?.showSelfServe === false) { return "managed_billing"; } return undefined; } export async function getLimits(orgId: string) { if (!client) return undefined; try { const result = await client.currentPlan(orgId); if (!result.success) { recordPlatformFailure("getLimits", "no_success"); return undefined; } return result.v3Subscription?.plan?.limits; } catch (_e) { recordPlatformFailure("getLimits", "caught"); return undefined; } } export async function getLimit(orgId: string, limit: keyof Limits, fallback: number) { const limits = await getLimits(orgId); if (!limits) return fallback; const result = limits[limit]; if (!result) return fallback; if (typeof result === "number") return result; if (typeof result === "object" && "number" in result) return result.number; return fallback; } export async function getDefaultEnvironmentConcurrencyLimit( organizationId: string, environmentType: RuntimeEnvironmentType ): Promise { if (!client) { const org = await $replica.organization.findFirst({ where: { id: organizationId, }, select: { maximumConcurrencyLimit: true, }, }); if (!org) throw new Error("Organization not found"); return org.maximumConcurrencyLimit; } const result = await client.currentPlan(organizationId); if (!result.success) throw new Error("Error getting current plan"); const limit = getDefaultEnvironmentLimitFromPlan(environmentType, result); if (!limit) throw new Error("No plan found"); return limit; } export function getDefaultEnvironmentLimitFromPlan( environmentType: RuntimeEnvironmentType, plan: CurrentPlan ): number | undefined { if (!plan.v3Subscription?.plan) return undefined; switch (environmentType) { case "DEVELOPMENT": return plan.v3Subscription.plan.limits.concurrentRuns.development; case "STAGING": return plan.v3Subscription.plan.limits.concurrentRuns.staging; case "PREVIEW": return plan.v3Subscription.plan.limits.concurrentRuns.preview; case "PRODUCTION": return plan.v3Subscription.plan.limits.concurrentRuns.production; default: return plan.v3Subscription.plan.limits.concurrentRuns.number; } } export async function getCachedLimit(orgId: string, limit: keyof Limits, fallback: number) { return platformCache.limits.swr(`${orgId}:${limit}`, async () => { return getLimit(orgId, limit, fallback); }); } export async function customerPortalUrl(orgId: string, orgSlug: string) { if (!client) return undefined; try { return client.createPortalSession(orgId, { returnUrl: `${env.APP_ORIGIN}${organizationBillingPath({ slug: orgSlug })}`, }); } catch (_e) { recordPlatformFailure("customerPortalUrl", "caught"); return undefined; } } export async function getPlans() { if (!client) return undefined; try { const result = await client.plans(); if (!result.success) { recordPlatformFailure("getPlans", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("getPlans", "caught"); return undefined; } } export async function setPlan( organization: { id: string; slug: string }, request: Request, callerPath: string, plan: SetPlanBody, opts?: { invalidateBillingCache?: (orgId: string) => void; // Runs only after the Free plan has actually been provisioned, with the // redirect it will return — the single success path where side effects that // depend on a working free-plan entitlement (e.g. redeeming a promo code) // are safe. It never fires on an error path, so callers can't act on a // plan change that didn't happen. onFreePlanProvisioned?: (response: Response) => void | Promise; } ) { if (!client) { return redirectWithErrorMessage(callerPath, request, "Error setting plan", { ephemeral: false, }); } const [error, result] = await tryCatch(client.setPlan(organization.id, plan)); if (error) { return redirectWithErrorMessage(callerPath, request, error.message, { ephemeral: false }); } if (!result) { return redirectWithErrorMessage(callerPath, request, "Error setting plan", { ephemeral: false, }); } if (!result.success) { return redirectWithErrorMessage(callerPath, request, result.error, { ephemeral: false }); } switch (result.action) { case "free_connect_required": case "free_connected": { // Selecting Free provisions the plan directly, so any free result is a success. opts?.invalidateBillingCache?.(organization.id); platformCache.entitlement.remove(organization.id).catch(() => {}); const response = redirect(newProjectPath(organization, "You're on the Free plan.")); await opts?.onFreePlanProvisioned?.(response); return response; } case "create_subscription_flow_start": { return redirect(result.checkoutUrl); } case "updated_subscription": { // Invalidate billing cache since subscription changed opts?.invalidateBillingCache?.(organization.id); platformCache.entitlement.remove(organization.id).catch(() => {}); return redirectWithSuccessMessage(callerPath, request, "Subscription updated successfully."); } case "canceled_subscription": { // Invalidate billing cache since subscription was canceled opts?.invalidateBillingCache?.(organization.id); platformCache.entitlement.remove(organization.id).catch(() => {}); return redirectWithSuccessMessage(callerPath, request, "Subscription canceled."); } } // Unrecognised action shape — surface an error rather than falling through to // an implicit undefined return, so callers always get a Response back. return redirectWithErrorMessage(callerPath, request, "Error setting plan", { ephemeral: false, }); } export async function setConcurrencyAddOn(organizationId: string, amount: number) { if (!client) return undefined; try { const result = await client.setAddOn(organizationId, { type: "concurrency", amount }); if (!result.success) { recordPlatformFailure("setConcurrencyAddOn", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("setConcurrencyAddOn", "caught"); return undefined; } } export async function setSeatsAddOn(organizationId: string, amount: number) { if (!client) return undefined; try { const result = await client.setAddOn(organizationId, { type: "seats", amount }); if (!result.success) { recordPlatformFailure("setSeatsAddOn", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("setSeatsAddOn", "caught"); return undefined; } } export async function setBranchesAddOn(organizationId: string, amount: number) { if (!client) return undefined; try { const result = await client.setAddOn(organizationId, { type: "branches", amount }); if (!result.success) { recordPlatformFailure("setBranchesAddOn", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("setBranchesAddOn", "caught"); return undefined; } } export async function setSchedulesAddOn(organizationId: string, amount: number) { if (!client) return undefined; try { const result = await client.setAddOn(organizationId, { type: "schedules", amount }); if (!result.success) { recordPlatformFailure("setSchedulesAddOn", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("setSchedulesAddOn", "caught"); return undefined; } } export async function getUsage(organizationId: string, { from, to }: { from: Date; to: Date }) { if (!client) return undefined; try { const result = await client.usage(organizationId, { from, to }); if (!result.success) { recordPlatformFailure("getUsage", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("getUsage", "caught"); return undefined; } } export async function getCachedUsage( organizationId: string, { from, to }: { from: Date; to: Date } ) { if (!client) return undefined; try { const result = await platformCache.usage.swr( `${organizationId}:${from.toISOString()}:${to.toISOString()}`, async () => { const usageResponse = await getUsage(organizationId, { from, to }); return usageResponse; } ); return result.val; } catch (_e) { recordPlatformFailure("getCachedUsage", "caught"); return undefined; } } export async function getUsageSeries(organizationId: string, params: UsageSeriesParams) { if (!client) return undefined; try { const result = await client.usageSeries(organizationId, params); if (!result.success) { recordPlatformFailure("getUsageSeries", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("getUsageSeries", "caught"); return undefined; } } export async function reportInvocationUsage( organizationId: string, costInCents: number, additionalData?: Record ) { if (!client) return undefined; try { const result = await client.reportInvocationUsage({ organizationId, costInCents, additionalData, }); if (!result.success) { recordPlatformFailure("reportInvocationUsage", "no_success"); return undefined; } return result; } catch (_e) { recordPlatformFailure("reportInvocationUsage", "caught"); return undefined; } } export async function reportComputeUsage(request: Request) { if (!client) return undefined; return fetch(`${process.env.BILLING_API_URL}/api/v1/usage/ingest/compute`, { method: "POST", headers: request.headers, body: await request.text(), }); } export async function getEntitlement( organizationId: string ): Promise { if (!client) return undefined; // Errors must be caught inside the loader — @unkey/cache passes the loader // promise to waitUntil() with no .catch(), so an unhandled rejection during // background SWR revalidation would crash the process. Returning undefined // on error tells SWR not to commit a fail-open value to the cache, which // prevents transient billing errors from overwriting a legitimate // hasAccess: false entry. The fail-open default is applied *outside* the // SWR call so it never becomes a cached access decision. const result = await platformCache.entitlement.swr(organizationId, async () => { try { const response = await client.fetch( `/api/v1/orgs/${organizationId}/usage/entitlement`, asPlatformSchema(EntitlementResultSchema) ); if (!response.success) { recordPlatformFailure("getEntitlement", "no_success"); return undefined; } return response; } catch (_e) { recordPlatformFailure("getEntitlement", "caught"); return undefined; } }); if (result.err || result.val === undefined) { return { hasAccess: true as const, }; } return result.val; } export type PromoCreditsData = { grantedCents: number; remainingCents: number; expiresAt: string | null; }; /** * Remaining promo/credit-grant balance for an org, or null when it has none. * Billing-side gating keeps this cheap for orgs without credits; the SWR cache * keeps repeated dashboard loads off the network. Fails closed to null so the * display is simply hidden on any error — never blocks the page. */ export async function getPromoCredits(organizationId: string): Promise { if (!client) return null; const result = await platformCache.promoCredits.swr(organizationId, async () => { try { const response = await client.promoCredits(organizationId); if (!response.success) { recordPlatformFailure("promoCredits", "no_success"); // Return undefined (not null) so SWR doesn't cache a transient failure // as "no credits" and hide the display for the stale TTL. null is // reserved for a successful "org has no promo credits" response. return undefined; } return response.promoCredits; } catch (_e) { recordPlatformFailure("promoCredits", "caught"); logger.error("promoCredits threw", { error: _e }); return undefined; } }); if (result.err || result.val === undefined) { return null; } return result.val; } export async function getBillingLimit( organizationId: string ): Promise { if (!client) return undefined; // Loader callback errors are caught below; also guard the SWR read itself so // Redis/cache infra failures cannot reject org-layout Promise.all callers. try { const result = await platformCache.billingLimit.swr(organizationId, async () => { try { const response = await client.fetch( `/api/v1/orgs/${organizationId}/billing-limit`, asPlatformSchema(BillingLimitResultSchema) ); if (!response.success) { recordPlatformFailure("getBillingLimit", "no_success"); return undefined; } return response; } catch (_e) { recordPlatformFailure("getBillingLimit", "caught"); return undefined; } }); if (result.err || result.val === undefined) { return undefined; } return result.val; } catch (_e) { recordPlatformFailure("getBillingLimit", "caught"); return undefined; } } export async function setBillingLimit( organizationId: string, config: UpdateBillingLimitRequest ): Promise { if (!client) return undefined; const response = await client.fetch( `/api/v1/orgs/${organizationId}/billing-limit`, asPlatformSchema(BillingLimitResultSchema), { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(config), } ); if (!response.success) { recordPlatformFailure("setBillingLimit", "no_success"); throw new Error(response.error ?? "Error setting billing limit"); } invalidateBillingLimitCaches(organizationId); return response; } export async function resolveBillingLimit( organizationId: string, payload: ResolveBillingLimitRequest ): Promise { if (!client) return undefined; const response = await client.fetch( `/api/v1/orgs/${organizationId}/billing-limit/resolve`, asPlatformSchema(BillingLimitResultSchema), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), } ); if (!response.success) { recordPlatformFailure("resolveBillingLimit", "no_success"); throw new Error(response.error ?? "Error resolving billing limit"); } invalidateBillingLimitCaches(organizationId); return response; } /** Admin: orgs currently in grace or rejected — used by reconciliation worker (Phase 2). */ export async function getActiveBillingLimits(): Promise { if (!client) return undefined; try { const response = await client.fetch( `/api/v1/billing-limits/active`, asPlatformSchema(BillingLimitsActiveResultSchema) ); if (!response.success) { recordPlatformFailure("getActiveBillingLimits", "no_success"); return undefined; } return response; } catch (_e) { recordPlatformFailure("getActiveBillingLimits", "caught"); return undefined; } } /** Admin: orgs with pending resolve side effects — used by reconciliation worker. */ export async function getPendingBillingLimitResolves(): Promise< BillingLimitsPendingResolvesResult | undefined > { if (!client) return undefined; try { const response = await client.fetch( `/api/v1/billing-limits/pending-resolves`, asPlatformSchema(BillingLimitsPendingResolvesResultSchema) ); if (!response.success) { recordPlatformFailure("getPendingBillingLimitResolves", "no_success"); return undefined; } return response; } catch (_e) { recordPlatformFailure("getPendingBillingLimitResolves", "caught"); return undefined; } } /** Admin: mark billing limit resolve side effects as completed after webapp convergence. */ export async function completeBillingLimitResolve( organizationId: string ): Promise<{ completed: boolean } | undefined> { if (!client) return undefined; const response = await client.fetch( `/api/v1/orgs/${organizationId}/billing-limit/resolve-complete`, asPlatformSchema(z.object({ completed: z.boolean() })), { method: "POST", } ); if (!response.success) { recordPlatformFailure("completeBillingLimitResolve", "no_success"); throw new Error(response.error ?? "Error completing billing limit resolve"); } return response; } export async function getBillingAlerts( organizationId: string ): Promise { if (!client) return undefined; const result = await client.getBillingAlerts(organizationId); if (!result.success) { recordPlatformFailure("getBillingAlert", "no_success"); throw new Error("Error getting billing alert"); } return result; } export async function setBillingAlert( organizationId: string, alert: UpdateBillingAlertsRequest ): Promise { if (!client) return undefined; const result = await client.updateBillingAlerts(organizationId, alert); if (!result.success) { recordPlatformFailure("setBillingAlert", "no_success"); throw new Error("Error setting billing alert"); } return result; } export async function generateRegistryCredentials( projectId: string, region: "us-east-1" | "eu-central-1" ) { if (!client) return undefined; const result = await client.generateRegistryCredentials(projectId, region); if (!result.success) { recordPlatformFailure("generateRegistryCredentials", "no_success"); throw new Error("Failed to generate registry credentials"); } return result; } export async function enqueueBuild( projectId: string, deploymentId: string, artifactKey: string, options: { skipPromotion?: boolean; configFilePath?: string; } ) { if (!client) return undefined; const result = await client.enqueueBuild(projectId, { deploymentId, artifactKey, options }); if (!result.success) { recordPlatformFailure("enqueueBuild", "no_success"); throw new Error("Failed to enqueue build"); } return result; } export async function getPrivateLinks( organizationId: string ): Promise { if (!client) return undefined; const [error, result] = await tryCatch(client.getPrivateLinks(organizationId)); if (error) { recordPlatformFailure("getPrivateLinks", "caught"); return undefined; } if (!result.success) { recordPlatformFailure("getPrivateLinks", "no_success"); return undefined; } return result; } export async function createPrivateLink( organizationId: string, body: CreatePrivateLinkConnectionBody ): Promise { if (!client) throw new Error("Platform client not configured"); const [error, result] = await tryCatch(client.createPrivateLink(organizationId, body)); if (error) { recordPlatformFailure("createPrivateLink", "caught"); throw error; } if (!result.success) { recordPlatformFailure("createPrivateLink", "no_success"); throw new Error(result.error ?? "Failed to create private link"); } return result; } export async function deletePrivateLink( organizationId: string, connectionId: string ): Promise { if (!client) throw new Error("Platform client not configured"); const [error, result] = await tryCatch(client.deletePrivateLink(organizationId, connectionId)); if (error) { recordPlatformFailure("deletePrivateLink", "caught"); throw error; } if (!result.success) { recordPlatformFailure("deletePrivateLink", "no_success"); throw new Error(result.error ?? "Failed to delete private link"); } } export async function getPrivateLinkRegions( organizationId: string ): Promise { if (!client) return undefined; const [error, result] = await tryCatch(client.getPrivateLinkRegions(organizationId)); if (error) { recordPlatformFailure("getPrivateLinkRegions", "caught"); return undefined; } if (!result.success) { recordPlatformFailure("getPrivateLinkRegions", "no_success"); return undefined; } return result; } export async function triggerInitialDeployment( projectId: string, options: { environment: "preview" | "prod" | "staging" } ): Promise { if (!client) return; const [error, result] = await tryCatch(client.triggerInitialDeployment(projectId, options)); if (error) { logger.warn("Error triggering initial deployment", { projectId, environment: options.environment, error, }); return; } if (!result.success) { logger.warn("Failed to trigger initial deployment", { projectId, environment: options.environment, error: result.error, }); } } export type { BillingLimitConfig, BillingLimitPageData, BillingLimitResult, BillingLimitState, BillingLimitsActiveResult, EntitlementResult, ResolveBillingLimitRequest, UpdateBillingLimitRequest, } from "~/services/billingLimit.schemas"; export function isCloud(): boolean { const acceptableHosts = [ "https://cloud.trigger.dev", "https://test-cloud.trigger.dev", "https://internal.trigger.dev", ]; if (acceptableHosts.includes(env.LOGIN_ORIGIN)) { return true; } if (process.env.CLOUD_ENV === "development" && process.env.NODE_ENV === "development") { return true; } return false; }