feat(webapp): promo credits — /promo signup landing, redeem at plan selection, usage display (#4138)

## What & why

Signup promo credits. A new logged-out `/promo?code=<code>` landing page
validates the code and carries it through signup via a cookie. When the
new organization is activated by selecting a plan, the code is redeemed
and its credits are applied; the usage page then shows the remaining
promo credits and their expiry.

## Notes

- The code is redeemed at **plan selection**, not org creation: the
credit grant targets the org's usage allowance, which only exists once a
plan is selected — applying at creation would have nothing to grant
onto. Redemption is best-effort and never blocks plan selection.
- Pairs with the corresponding billing-service change (promo code
validate/apply/credits + grant issuance); the two are released together.

## Testing

Verified locally end to end: `/promo` shows the offer, a new account
carries the code through signup, selecting the Free plan redeems it, and
the usage page shows the remaining credits.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Matt Aitken
2026-07-10 16:48:39 +01:00
committed by GitHub
parent 983bd03131
commit 48a0b83ec6
10 changed files with 506 additions and 52 deletions
+6
View File
@@ -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.
+29 -18
View File
@@ -36,7 +36,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<QuoteType | null>(null);
useEffect(() => {
const randomIndex = Math.floor(Math.random() * quotes.length);
@@ -62,23 +69,27 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
</div>
</div>
<div className="hidden grid-rows-[1fr_auto] pb-6 lg:grid">
<div className="flex h-full flex-col items-center justify-center px-16">
<Header3 className="relative text-center text-2xl font-normal leading-8 text-text-dimmed transition before:relative before:right-1 before:top-0 before:text-6xl before:text-charcoal-750 before:content-['❝'] lg-height:text-xl md-height:text-lg">
{randomQuote?.quote}
</Header3>
<Paragraph className="mt-4 text-text-dimmed/60">{randomQuote?.person}</Paragraph>
</div>
<div className="flex flex-col items-center gap-4 px-8">
<Paragraph>Trusted by developers at</Paragraph>
<div className="flex w-full flex-wrap items-center justify-center gap-x-6 gap-y-3 text-text-faint xl:justify-between xl:gap-0">
<LyftLogo className="w-11" />
<UnkeyLogo />
<MiddayLogo />
<AppsmithLogo />
<CalComLogo />
<TldrawLogo />
</div>
</div>
{rightContent ?? (
<>
<div className="flex h-full flex-col items-center justify-center px-16">
<Header3 className="relative text-center text-2xl font-normal leading-8 text-text-dimmed transition before:relative before:right-1 before:top-0 before:text-6xl before:text-charcoal-750 before:content-['❝'] lg-height:text-xl md-height:text-lg">
{randomQuote?.quote}
</Header3>
<Paragraph className="mt-4 text-text-dimmed/60">{randomQuote?.person}</Paragraph>
</div>
<div className="flex flex-col items-center gap-4 px-8">
<Paragraph>Trusted by developers at</Paragraph>
<div className="flex w-full flex-wrap items-center justify-center gap-x-6 gap-y-3 text-text-faint xl:justify-between xl:gap-0">
<LyftLogo className="w-11" />
<UnkeyLogo />
<MiddayLogo />
<AppsmithLogo />
<CalComLogo />
<TldrawLogo />
</div>
</div>
</>
)}
</div>
</main>
);
@@ -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<typeof loader>();
const { usage, tasks, months, isCurrentMonth, promoCredits } =
useTypedLoaderData<typeof loader>();
const currentPlan = useCurrentPlan();
const billingLimit = useBillingLimit();
const planLimitCents = currentPlan?.v3Subscription?.plan?.limits.includedUsage ?? 0;
@@ -139,6 +153,45 @@ export default function Page() {
))
}
</Select>
{promoCredits && (
<div className="flex flex-col gap-1 border-t border-grid-dimmed p-3">
<div className="flex items-end gap-8">
<div className="flex flex-col gap-1">
<Header2 className="whitespace-nowrap">Promo credits</Header2>
<p className="whitespace-nowrap text-3xl font-medium text-text-bright">
{formatCurrency(promoCredits.remainingCents / 100, false)}
</p>
</div>
<div className="flex w-full flex-1 flex-col gap-1 pb-1">
<div className="h-2 w-full overflow-hidden rounded-full bg-charcoal-700">
<div
className="h-full rounded-full bg-blue-500"
style={{
width: `${
promoCredits.grantedCents > 0
? Math.min(
100,
Math.max(
0,
(promoCredits.remainingCents / promoCredits.grantedCents) * 100
)
)
: 0
}%`,
}}
/>
</div>
<Paragraph variant="extra-small" className="text-text-dimmed">
{formatCurrency(promoCredits.remainingCents / 100, false)} of{" "}
{formatCurrency(promoCredits.grantedCents / 100, false)} remaining
{promoCredits.expiresAt
? ` · expires ${creditExpiryFormatter.format(new Date(promoCredits.expiresAt))}`
: ""}
</Paragraph>
</div>
</div>
</div>
)}
<div className="flex w-full flex-col gap-2 border-t border-grid-dimmed p-3">
<Suspense fallback={<Spinner />}>
<Await
@@ -171,6 +224,7 @@ export default function Page() {
</Suspense>
</div>
</div>
<div className="px-3">
<Card>
<Card.Header>Usage by day</Card.Header>
@@ -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));
+177
View File
@@ -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 (
<Fieldset className="w-full">
<div className="flex flex-col items-center gap-y-3">
{showGithubAuth && (
<Form action="/auth/github" method="post" className="w-full">
<Button
type="submit"
variant="secondary/extra-large"
fullWidth
data-action="continue with github"
>
<GitHubLightIcon className="mr-2 size-5" />
<span className="text-text-bright">Continue with GitHub</span>
</Button>
</Form>
)}
{showGoogleAuth && (
<Form action="/auth/google" method="post" className="w-full">
<Button
type="submit"
variant="secondary/extra-large"
fullWidth
data-action="continue with google"
>
<GoogleLogo className="mr-2 size-5" />
<span className="text-text-bright">Continue with Google</span>
</Button>
</Form>
)}
<LinkButton
to="/login/magic"
variant="secondary/extra-large"
fullWidth
data-action="continue with email"
className="text-text-bright"
>
<EnvelopeIcon className="mr-2 size-5 text-text-bright" />
Continue with Email
</LinkButton>
</div>
<Paragraph variant="extra-small" className="mt-2 text-center">
By signing up you agree to our{" "}
<TextLink href="https://trigger.dev/legal" target="_blank">
terms
</TextLink>{" "}
and{" "}
<TextLink href="https://trigger.dev/legal/privacy" target="_blank">
privacy
</TextLink>{" "}
policy.
</Paragraph>
</Fieldset>
);
}
export default function PromoPage() {
const data = useTypedLoaderData<typeof loader>();
return (
<LoginPageLayout>
<div className="flex w-full flex-col">
{data.view === "signed_in" ? (
<>
<Header2 className="sm:text-2xl md:text-3xl lg:text-4xl" spacing>
Promo codes are for new accounts
</Header2>
<Paragraph variant="base" spacing>
You're already signed in. Promo credits can only be added to a brand-new account.
</Paragraph>
<LinkButton to="/" variant="secondary/medium">
Go to dashboard
</LinkButton>
</>
) : (
<>
<Header2 className="sm:text-2xl md:text-3xl lg:text-4xl" spacing>
{data.view === "valid"
? `Claim ${formatDollars(data.amountInCents)} credits`
: "Create your account"}
</Header2>
{data.view === "valid" ? (
<Paragraph variant="base" spacing>
These are only available for new accounts on the Free plan.
{formatExpiry(data.expiresAt)
? ` The credits expire on ${formatExpiry(data.expiresAt)}.`
: ""}
</Paragraph>
) : (
<Callout variant="warning" className="mb-6 w-full">
That promo code isn't valid. You can still sign up below but credits won't be added.
</Callout>
)}
<SignInForm showGithubAuth={data.showGithubAuth} showGoogleAuth={data.showGoogleAuth} />
</>
)}
</div>
</LoginPageLayout>
);
}
@@ -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,26 @@ 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. This runs only
// once the Free plan has actually been provisioned (the grant target), so a
// failed plan change never burns the one-time code. Best-effort: it must
// never change the plan-selection outcome.
onFreePlanProvisioned: async (response) => {
const promoCode = await getPromoCodeFromCookie(request);
if (!promoCode) {
return;
}
const applied = await applyPromoCode(organization.id, user.id, promoCode);
if (applied?.applied) {
bustPromoCreditsCache(organization.id);
response.headers.append("Set-Cookie", await clearPromoCodeCookie());
}
},
});
return result;
}
);
+152 -14
View File
@@ -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<ValidatedPromoCode | undefined> {
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<AppliedPromoCode | undefined> {
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<PromoCreditsData | null>(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({
@@ -417,7 +500,15 @@ export async function setPlan(
request: Request,
callerPath: string,
plan: SetPlanBody,
opts?: { invalidateBillingCache?: (orgId: string) => void }
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<void>;
}
) {
if (!client) {
return redirectWithErrorMessage(callerPath, request, "Error setting plan", {
@@ -447,7 +538,9 @@ export async function setPlan(
// Selecting Free provisions the plan directly, so any free result is a success.
opts?.invalidateBillingCache?.(organization.id);
platformCache.entitlement.remove(organization.id).catch(() => {});
return redirect(newProjectPath(organization, "You're on the Free plan."));
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);
@@ -465,6 +558,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 +757,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<PromoCreditsData | null> {
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<BillingLimitResult | undefined> {
@@ -980,8 +1118,8 @@ export type {
BillingLimitConfig,
BillingLimitPageData,
BillingLimitResult,
BillingLimitsActiveResult,
BillingLimitState,
BillingLimitsActiveResult,
EntitlementResult,
ResolveBillingLimitRequest,
UpdateBillingLimitRequest,
@@ -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<string> {
return await promoCodeCookie.serialize(code);
}
export async function getPromoCodeFromCookie(request: Request): Promise<string | null> {
const value = await promoCodeCookie.parse(request.headers.get("Cookie"));
return typeof value === "string" && value.length > 0 ? value : null;
}
export async function clearPromoCodeCookie(): Promise<string> {
return await promoCodeCookie.serialize("", { maxAge: 0 });
}
+1 -1
View File
@@ -126,7 +126,7 @@
"@trigger.dev/core": "workspace:*",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/otlp-importer": "workspace:*",
"@trigger.dev/platform": "1.0.29",
"@trigger.dev/platform": "1.2.0",
"@trigger.dev/plugins": "workspace:*",
"@trigger.dev/rbac": "workspace:*",
"@trigger.dev/redis-worker": "workspace:*",
+35 -14
View File
@@ -504,8 +504,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
@@ -3074,10 +3074,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'}
@@ -3177,6 +3185,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'}
@@ -8320,8 +8332,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==}
@@ -16411,9 +16425,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==}
@@ -18580,8 +18591,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': {}
@@ -18689,6 +18706,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
@@ -24566,9 +24589,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:
@@ -29249,8 +29272,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
@@ -30288,7 +30311,7 @@ snapshots:
node-abi@3.89.0:
dependencies:
semver: 7.8.1
semver: 7.7.3
optional: true
node-abort-controller@3.1.1: {}
@@ -34109,8 +34132,6 @@ snapshots:
dependencies:
zod: 3.25.76
zod@3.23.8: {}
zod@3.25.76: {}
zwitch@2.0.4: {}