diff --git a/.server-changes/promo-credits.md b/.server-changes/promo-credits.md new file mode 100644 index 0000000000..9ddf549acd --- /dev/null +++ b/.server-changes/promo-credits.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: feature +--- + +Promo credits: a /promo signup landing page, redeeming a promo code when a new org selects a plan, and showing remaining credits on the usage page. diff --git a/apps/webapp/app/components/LoginPageLayout.tsx b/apps/webapp/app/components/LoginPageLayout.tsx index 323faf4ea2..63fbafc57e 100644 --- a/apps/webapp/app/components/LoginPageLayout.tsx +++ b/apps/webapp/app/components/LoginPageLayout.tsx @@ -38,7 +38,14 @@ const quotes: QuoteType[] = [ }, ]; -export function LoginPageLayout({ children }: { children: React.ReactNode }) { +export function LoginPageLayout({ + children, + rightContent, +}: { + children: React.ReactNode; + /** Replaces the default testimonials panel on the right (e.g. a promo highlight). */ + rightContent?: React.ReactNode; +}) { const [randomQuote, setRandomQuote] = useState(null); useEffect(() => { const randomIndex = Math.floor(Math.random() * quotes.length); @@ -69,23 +76,27 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
-
- - {randomQuote?.quote} - - {randomQuote?.person} -
-
- Trusted by developers at -
- - - - - - -
-
+ {rightContent ?? ( + <> +
+ + {randomQuote?.quote} + + {randomQuote?.person} +
+
+ Trusted by developers at +
+ + + + + + +
+
+ + )}
); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx index fb0c5d4d83..2cc8b01110 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx @@ -29,6 +29,7 @@ import { prisma } from "~/db.server"; import { featuresForRequest } from "~/features.server"; import { useSearchParams } from "~/hooks/useSearchParam"; import { UsagePresenter, type UsageSeriesData } from "~/presenters/v3/UsagePresenter.server"; +import { getPromoCredits } from "~/services/platform.v3.server"; import { requireUserId } from "~/services/session.server"; import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter"; import { useBillingLimit } from "~/hooks/useOrganizations"; @@ -81,14 +82,26 @@ export async function loader({ params, request }: LoaderFunctionArgs) { startDate, }); + // Credit-grant balance (promo now, other grant types later). Cheap + cached + + // fails to null, and applies to any org with grants — not gated on plan tier. + const promoCredits = await getPromoCredits(organization.id); + return typeddefer({ usage, tasks, months, isCurrentMonth: startDate.toISOString() === months[0].toISOString(), + promoCredits, }); } +const creditExpiryFormatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "utc", +}); + const monthDateFormatter = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", @@ -96,7 +109,8 @@ const monthDateFormatter = new Intl.DateTimeFormat("en-US", { }); export default function Page() { - const { usage, tasks, months, isCurrentMonth } = useTypedLoaderData(); + const { usage, tasks, months, isCurrentMonth, promoCredits } = + useTypedLoaderData(); const currentPlan = useCurrentPlan(); const billingLimit = useBillingLimit(); const planLimitCents = currentPlan?.v3Subscription?.plan?.limits.includedUsage ?? 0; @@ -139,6 +153,45 @@ export default function Page() { )) } + {promoCredits && ( +
+
+
+ Promo credits +

+ {formatCurrency(promoCredits.remainingCents / 100, false)} +

+
+
+
+
0 + ? Math.min( + 100, + Math.max( + 0, + (promoCredits.remainingCents / promoCredits.grantedCents) * 100 + ) + ) + : 0 + }%`, + }} + /> +
+ + {formatCurrency(promoCredits.remainingCents / 100, false)} of{" "} + {formatCurrency(promoCredits.grantedCents / 100, false)} remaining + {promoCredits.expiresAt + ? ` · expires ${creditExpiryFormatter.format(new Date(promoCredits.expiresAt))}` + : ""} + +
+
+
+ )}
}>
+
Usage by day diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index d917265d9e..462621dd34 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -79,6 +79,10 @@ export const action: ActionFunction = async ({ request }) => { avatar, }); + // A promo code carried over from the /promo landing page (via cookie) is + // redeemed later, once the org is activated through plan selection and its + // usage entitlement exists — not here, where there's nothing to grant onto. + const url = new URL(request.url); const code = url.searchParams.get("code"); const configurationId = url.searchParams.get("configurationId"); @@ -94,8 +98,7 @@ export const action: ActionFunction = async ({ request }) => { if (next) { params.set("next", next); } - const redirectUrl = `${organizationPath(organization)}/projects/new?${params.toString()}`; - return redirect(redirectUrl); + return redirect(`${organizationPath(organization)}/projects/new?${params.toString()}`); } return redirect(organizationPath(organization)); diff --git a/apps/webapp/app/routes/promo.tsx b/apps/webapp/app/routes/promo.tsx new file mode 100644 index 0000000000..182f1c0318 --- /dev/null +++ b/apps/webapp/app/routes/promo.tsx @@ -0,0 +1,177 @@ +import { EnvelopeIcon } from "@heroicons/react/20/solid"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; +import { Form } from "@remix-run/react"; +import { GitHubLightIcon } from "@trigger.dev/companyicons"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { GoogleLogo } from "~/assets/logos/GoogleLogo"; +import { LoginPageLayout } from "~/components/LoginPageLayout"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; +import { Fieldset } from "~/components/primitives/Fieldset"; +import { Header2 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { TextLink } from "~/components/primitives/TextLink"; +import { isGithubAuthSupported, isGoogleAuthSupported } from "~/services/auth.server"; +import { validatePromoCode } from "~/services/platform.v3.server"; +import { setPromoCodeCookie } from "~/services/promoCode.server"; +import { getUserId } from "~/services/session.server"; +import { requestUrl } from "~/utils/requestUrl.server"; + +export const meta: MetaFunction = () => [{ title: "Claim your Trigger.dev credits" }]; + +export async function loader({ request }: LoaderFunctionArgs) { + const userId = await getUserId(request); + const url = requestUrl(request); + const code = url.searchParams.get("code")?.trim() || null; + + const authMethods = { + showGithubAuth: isGithubAuthSupported, + showGoogleAuth: isGoogleAuthSupported, + }; + + // Credits are only granted to brand-new accounts, so an already-signed-in + // user can't redeem a code. + if (userId) { + return typedjson({ view: "signed_in" as const, ...authMethods }); + } + + if (!code) { + return typedjson({ view: "invalid" as const, ...authMethods }); + } + + const validated = await validatePromoCode(code); + if (!validated || !validated.valid) { + return typedjson({ view: "invalid" as const, ...authMethods }); + } + + // Stash the code so it survives the OAuth round-trip and can be applied once + // the new org selects a plan. + return typedjson( + { + view: "valid" as const, + amountInCents: validated.amountInCents ?? 0, + expiresAt: validated.expiresAt ?? null, + ...authMethods, + }, + { headers: { "Set-Cookie": await setPromoCodeCookie(code) } } + ); +} + +function formatDollars(cents: number) { + const dollars = cents / 100; + return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`; +} + +function formatExpiry(iso: string | null) { + if (!iso) return null; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return null; + return date.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); +} + +function SignInForm({ + showGithubAuth, + showGoogleAuth, +}: { + showGithubAuth: boolean; + showGoogleAuth: boolean; +}) { + return ( +
+
+ {showGithubAuth && ( +
+ +
+ )} + {showGoogleAuth && ( +
+ +
+ )} + + + Continue with Email + +
+ + By signing up you agree to our{" "} + + terms + {" "} + and{" "} + + privacy + {" "} + policy. + +
+ ); +} + +export default function PromoPage() { + const data = useTypedLoaderData(); + + return ( + +
+ {data.view === "signed_in" ? ( + <> + + Promo codes are for new accounts + + + You're already signed in. Promo credits can only be added to a brand-new account. + + + Go to dashboard + + + ) : ( + <> + + {data.view === "valid" + ? `Claim ${formatDollars(data.amountInCents)} credits` + : "Create your account"} + + {data.view === "valid" ? ( + + These are only available for new accounts. + {formatExpiry(data.expiresAt) + ? ` The credits expire on ${formatExpiry(data.expiresAt)}.` + : ""} + + ) : ( + + That promo code isn't valid. You can still sign up below but credits won't be added. + + )} + + + )} +
+
+ ); +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index 02bd91cb9d..617646888f 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -34,7 +34,8 @@ import { prisma } from "~/db.server"; import { redirectWithErrorMessage } from "~/models/message.server"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; import { logger } from "~/services/logger.server"; -import { setPlan } from "~/services/platform.v3.server"; +import { applyPromoCode, bustPromoCreditsCache, setPlan } from "~/services/platform.v3.server"; +import { clearPromoCodeCookie, getPromoCodeFromCookie } from "~/services/promoCode.server"; import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder"; import { engine } from "~/v3/runEngine.server"; import { cn } from "~/utils/cn"; @@ -153,9 +154,25 @@ export const action = dashboardAction( } } - return await setPlan(organization, request, form.callerPath, payload, { + const result = await setPlan(organization, request, form.callerPath, payload, { invalidateBillingCache: engine.invalidateBillingCache.bind(engine), }); + + // Redeem a promo code carried from the /promo landing page now that selecting + // a plan has provisioned the org's usage entitlement (the grant target). + // Best-effort: it must never change the plan-selection outcome. + if (form.type === "free") { + const promoCode = await getPromoCodeFromCookie(request); + if (promoCode) { + const applied = await applyPromoCode(organization.id, user.id, promoCode); + if (applied?.applied) { + bustPromoCreditsCache(organization.id); + result.headers.append("Set-Cookie", await clearPromoCodeCookie()); + } + } + } + + return result; } ); diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index 616e2a011a..5c30ad3856 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -1,5 +1,3 @@ -import { createLRUMemoryStore } from "@internal/cache"; -import { metrics } from "@opentelemetry/api"; import { MachinePresetName, tryCatch } from "@trigger.dev/core/v3"; import type { RuntimeEnvironmentType } from "@trigger.dev/database"; import { @@ -8,7 +6,6 @@ import { machines as machinesFromPlatform, type BillingAlertsResult, type CreatePrivateLinkConnectionBody, - type CurrentPlan, type Limits, type MachineCode, type PrivateLinkConnection, @@ -18,20 +15,14 @@ import { type UpdateBillingAlertsRequest, type UsageResult, type UsageSeriesParams, + type CurrentPlan, } from "@trigger.dev/platform"; -import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; -import { existsSync, readFileSync } from "node:fs"; -import { redirect } from "remix-typedjson"; -import { z } from "zod"; -import { $replica } from "~/db.server"; -import { env } from "~/env.server"; -import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { - asPlatformSchema, BillingLimitResultSchema, BillingLimitsActiveResultSchema, BillingLimitsPendingResolvesResultSchema, EntitlementResultSchema, + asPlatformSchema, type BillingLimitResult, type BillingLimitsActiveResult, type BillingLimitsPendingResolvesResult, @@ -39,10 +30,19 @@ import { 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) { @@ -82,6 +82,78 @@ 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); @@ -119,6 +191,11 @@ function initializePlatformCache() { fresh: 60_000, stale: 120_000, }), + promoCredits: new Namespace(ctx, { + stores: [memory, redisCacheStore], + fresh: 60_000, + stale: 120_000, + }), }); return cache; @@ -135,6 +212,12 @@ 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({ @@ -465,6 +548,12 @@ export async function setPlan( 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) { @@ -658,6 +747,45 @@ export async function getEntitlement( 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 { @@ -980,8 +1108,8 @@ export type { BillingLimitConfig, BillingLimitPageData, BillingLimitResult, - BillingLimitsActiveResult, BillingLimitState, + BillingLimitsActiveResult, EntitlementResult, ResolveBillingLimitRequest, UpdateBillingLimitRequest, diff --git a/apps/webapp/app/services/promoCode.server.ts b/apps/webapp/app/services/promoCode.server.ts new file mode 100644 index 0000000000..af8f5424a7 --- /dev/null +++ b/apps/webapp/app/services/promoCode.server.ts @@ -0,0 +1,26 @@ +import { createCookie } from "@remix-run/node"; +import { env } from "~/env.server"; + +// Carries a promo code from the landing page through signup to first-org +// creation. httpOnly + sameSite=lax so it survives the OAuth round-trip, +// matching the existing redirect-to cookie. +export const promoCodeCookie = createCookie("promo-code", { + maxAge: 60 * 60, // 1 hour — enough to complete signup + httpOnly: true, + sameSite: "lax", + secure: env.NODE_ENV === "production", + path: "/", +}); + +export async function setPromoCodeCookie(code: string): Promise { + return await promoCodeCookie.serialize(code); +} + +export async function getPromoCodeFromCookie(request: Request): Promise { + const value = await promoCodeCookie.parse(request.headers.get("Cookie")); + return typeof value === "string" && value.length > 0 ? value : null; +} + +export async function clearPromoCodeCookie(): Promise { + return await promoCodeCookie.serialize("", { maxAge: 0 }); +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index cede926281..d988e3bf2b 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -129,7 +129,7 @@ "@trigger.dev/rbac": "workspace:*", "@trigger.dev/sso": "workspace:*", "@trigger.dev/otlp-importer": "workspace:*", - "@trigger.dev/platform": "1.0.29", + "@trigger.dev/platform": "1.2.0", "@trigger.dev/redis-worker": "workspace:*", "@trigger.dev/sdk": "workspace:*", "@types/pg": "8.6.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1708fb773d..00d7437bc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,8 +573,8 @@ importers: specifier: workspace:* version: link:../../internal-packages/otlp-importer '@trigger.dev/platform': - specifier: 1.0.29 - version: 1.0.29 + specifier: 1.2.0 + version: 1.2.0(zod@3.25.76) '@trigger.dev/plugins': specifier: workspace:* version: link:../../packages/plugins @@ -3121,10 +3121,18 @@ packages: resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} @@ -3224,6 +3232,10 @@ packages: resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -8458,8 +8470,10 @@ packages: react: 18.3.1 react-dom: 18.3.1 - '@trigger.dev/platform@1.0.29': - resolution: {integrity: sha512-75lsz0igwY9tqWfT6U7Huj+94VWic3//B4Cux4muCzH/ZC8Hz22O9fsMe+R7JtQy7HsemG42R+Zwy5ITnSgFYg==} + '@trigger.dev/platform@1.2.0': + resolution: {integrity: sha512-Wa/XlMlmo1vhol5DEBYW1gMW4wgUUqr/ClDQb4IgwrRdPeLLmTpIVsgMHmYJXkERqR04xVaX7wBibWl4sW+1Hg==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -16936,9 +16950,6 @@ packages: peerDependencies: zod: ^3.18.0 - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -19105,8 +19116,14 @@ snapshots: dependencies: '@babel/types': 7.29.7 + '@babel/helper-string-parser@7.27.1': + optional: true + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': + optional: true + '@babel/helper-validator-identifier@7.29.7': {} '@babel/helper-validator-option@7.22.15': {} @@ -19214,6 +19231,12 @@ snapshots: '@babel/helper-validator-identifier': 7.29.7 to-fast-properties: 2.0.0 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + optional: true + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -25128,9 +25151,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@trigger.dev/platform@1.0.29': + '@trigger.dev/platform@1.2.0(zod@3.25.76)': dependencies: - zod: 3.23.8 + zod: 3.25.76 '@types/acorn@4.0.6': dependencies: @@ -30038,8 +30061,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.27.0 + '@babel/types': 7.29.0 source-map-js: 1.2.1 optional: true @@ -31079,7 +31102,7 @@ snapshots: node-abi@3.89.0: dependencies: - semver: 7.8.1 + semver: 7.7.3 optional: true node-abort-controller@3.1.1: {} @@ -35135,8 +35158,6 @@ snapshots: dependencies: zod: 3.25.76 - zod@3.23.8: {} - zod@3.25.76: {} zwitch@2.0.4: {}