From 8b039708ffcb03c728ac570ad41d2cee35cef369 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Mon, 26 May 2025 16:36:44 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth.ts | 4 +- apps/api/src/db/schema/deployment.ts | 51 ++++++-------- apps/api/src/db/schema/project.ts | 16 ++--- apps/api/src/db/schema/user.ts | 10 ++- apps/api/src/db/utils.ts | 31 +-------- .../lib/auth/upsert-or-link-user-account.ts | 5 +- .../src/lib/billing/upsert-stripe-pricing.ts | 34 +++------- .../lib/billing/upsert-stripe-subscription.ts | 20 +++--- packages/api-client/src/openapi.d.ts | 52 ++++++++++----- .../src/agentic-project-config-schema.ts | 34 ++++++---- packages/schemas/src/index.ts | 1 + packages/schemas/src/schemas.ts | 66 ++++++++++++------- packages/schemas/src/utils.ts | 28 ++++++++ .../src/validate-agentic-project-config.ts | 16 +++++ readme.md | 7 ++ 15 files changed, 205 insertions(+), 170 deletions(-) create mode 100644 packages/schemas/src/utils.ts diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts index ed1b6479..f57fe000 100644 --- a/apps/api/src/auth.ts +++ b/apps/api/src/auth.ts @@ -104,7 +104,7 @@ export const authRouter = issuer({ }, partialUser: { email: ghUser.email, - emailVerified: true, + isEmailVerified: true, name: ghUser.name || undefined, username: ghUser.login.toLowerCase(), image: ghUser.avatar_url @@ -118,7 +118,7 @@ export const authRouter = issuer({ }, partialUser: { email: value.email, - emailVerified: true + isEmailVerified: true } }) } else { diff --git a/apps/api/src/db/schema/deployment.ts b/apps/api/src/db/schema/deployment.ts index afefa23b..8546738e 100644 --- a/apps/api/src/db/schema/deployment.ts +++ b/apps/api/src/db/schema/deployment.ts @@ -1,6 +1,8 @@ import { + agenticProjectConfigSchema, type DeploymentOriginAdapter, deploymentOriginAdapterSchema, + pricingIntervalListSchema, type PricingPlanList, pricingPlanListSchema } from '@agentic/platform-schemas' @@ -29,6 +31,7 @@ import { createUpdateSchema, deploymentIdentifier, deploymentPrimaryId, + pricingIntervalEnum, projectId, teamId, timestamps, @@ -65,11 +68,10 @@ export const deployments = pgTable( onDelete: 'cascade' }), - // TODO: Tool definitions or OpenAPI spec - // services: jsonb().$type().default([]), + // TODO: Tool definitions + // tools: jsonb().$type().default([]), // TODO: metadata config (logo, keywords, examples, etc) - // TODO: openapi spec or tool definitions or mcp adapter // TODO: webhooks // TODO: third-party auth provider config // NOTE: will need consumer.authProviders as well as user.authProviders for @@ -84,7 +86,10 @@ export const deployments = pgTable( originAdapter: jsonb().$type().notNull(), // Array - pricingPlans: jsonb().$type().notNull() + pricingPlans: jsonb().$type().notNull(), + + // Which pricing intervals are supported for subscriptions to this project + pricingIntervals: pricingIntervalEnum().array().default(['month']).notNull() // coupons: jsonb().$type().default([]).notNull() }, @@ -135,8 +140,9 @@ export const deploymentSelectSchema = createSelectSchema(deployments, { message: 'Invalid deployment hash' }), + originAdapter: deploymentOriginAdapterSchema, pricingPlans: pricingPlanListSchema, - originAdapter: deploymentOriginAdapterSchema + pricingIntervals: pricingIntervalListSchema }) .omit({ originUrl: true @@ -159,33 +165,7 @@ export const deploymentSelectSchema = createSelectSchema(deployments, { .openapi('Deployment') export const deploymentInsertSchema = createInsertSchema(deployments, { - projectId: projectIdSchema, - - iconUrl: (schema) => - schema - .url() - .describe( - 'Logo image URL to use for this deployment. Logos should have a square aspect ratio.' - ), - - sourceUrl: (schema) => schema.url(), - - originUrl: (schema) => - schema.url().describe(`Base URL of the externally hosted origin API server. - -NOTE: Agentic currently only supports \`external\` API servers. If you'd like to host your API or MCP server on Agentic's infrastructure, please reach out to support@agentic.so.`), - - pricingPlans: pricingPlanListSchema.describe( - 'List of PricingPlans should be available as subscriptions for this deployment.' - ), - originAdapter: deploymentOriginAdapterSchema.default({ - location: 'external', - type: 'raw' - }) - // .optional() - - // TODO - // coupons: z.array(couponSchema).optional() + projectId: projectIdSchema }) .omit({ id: true, @@ -195,8 +175,15 @@ NOTE: Agentic currently only supports \`external\` API servers. If you'd like to userId: true, teamId: true }) + .extend({ + ...agenticProjectConfigSchema.shape + }) .strict() +// TODO: Deployments should be immutable, so we should not allow updates aside +// from publishing. But editing a project's description should be possible from +// the admin UI, so maybe we allow only updates to some properties? Or we +// denormalize these fields in `project`? export const deploymentUpdateSchema = createUpdateSchema(deployments) .pick({ deletedAt: true, diff --git a/apps/api/src/db/schema/project.ts b/apps/api/src/db/schema/project.ts index b5b375cd..02349c46 100644 --- a/apps/api/src/db/schema/project.ts +++ b/apps/api/src/db/schema/project.ts @@ -10,7 +10,6 @@ import { import { validators } from '@agentic/platform-validators' import { relations } from '@fisch0920/drizzle-orm' import { - boolean, index, integer, jsonb, @@ -18,7 +17,6 @@ import { text, uniqueIndex } from '@fisch0920/drizzle-orm/pg-core' -import { z } from '@hono/zod-openapi' import { deploymentIdSchema, @@ -69,21 +67,18 @@ export const projects = pgTable( applicationFeePercent: integer().default(20).notNull(), // TODO: This is going to need to vary from dev to prod - isStripeConnectEnabled: boolean().default(false).notNull(), - - // Which pricing intervals are supported for subscriptions to this project - pricingIntervals: pricingIntervalEnum() - .array() - .default(['month']) - .notNull(), + //isStripeConnectEnabled: boolean().default(false).notNull(), // Default pricing interval for subscriptions to this project + // Note: This is essentially hard-coded and not configurable by users for now. defaultPricingInterval: pricingIntervalEnum().default('month').notNull(), // Pricing currency used across all prices and subscriptions to this project pricingCurrency: pricingCurrencyEnum().default('usd').notNull(), - // All deployments share the same underlying proxy secret + // All deployments share the same underlying proxy secret, which allows + // origin servers to verify that requests are coming from Agentic's API + // gateway. _secret: text().notNull(), // Auth token used to access the platform API on behalf of this project @@ -179,7 +174,6 @@ export const projectSelectSchema = createSelectSchema(projects, { applicationFeePercent: (schema) => schema.nonnegative(), - pricingIntervals: z.array(pricingIntervalSchema).nonempty(), defaultPricingInterval: pricingIntervalSchema, _stripeProductIdMap: stripeProductIdMapSchema, diff --git a/apps/api/src/db/schema/user.ts b/apps/api/src/db/schema/user.ts index e8c3dc6c..663fbf33 100644 --- a/apps/api/src/db/schema/user.ts +++ b/apps/api/src/db/schema/user.ts @@ -19,8 +19,6 @@ import { userRoleEnum } from './common' -// This table is mostly managed by better-auth. - export const users = pgTable( 'users', { @@ -32,10 +30,10 @@ export const users = pgTable( name: text(), email: text().notNull().unique(), - emailVerified: boolean().default(false).notNull(), + isEmailVerified: boolean().default(false).notNull(), image: text(), - isStripeConnectEnabledByDefault: boolean().default(true).notNull(), + //isStripeConnectEnabledByDefault: boolean().default(true).notNull(), stripeCustomerId: stripeId() }, @@ -59,7 +57,7 @@ export const userSelectSchema = createSelectSchema(users) export const userUpdateSchema = createUpdateSchema(users) .pick({ name: true, - image: true, - isStripeConnectEnabledByDefault: true + image: true + //isStripeConnectEnabledByDefault: true }) .strict() diff --git a/apps/api/src/db/utils.ts b/apps/api/src/db/utils.ts index 2cce52e4..a600377a 100644 --- a/apps/api/src/db/utils.ts +++ b/apps/api/src/db/utils.ts @@ -1,8 +1,6 @@ import type { - PricingInterval, PricingPlan, - PricingPlanLineItem, - PricingPlanList + PricingPlanLineItem } from '@agentic/platform-schemas' import { hashObject } from '@agentic/platform-core' @@ -62,30 +60,3 @@ export function getStripePriceIdForPricingPlanLineItem({ return project._stripePriceIdMap[pricingPlanLineItemHash] } - -export function getPricingPlansByInterval({ - pricingInterval, - pricingPlans -}: { - pricingInterval: PricingInterval - pricingPlans: PricingPlanList -}): PricingPlan[] { - return pricingPlans.filter( - (pricingPlan) => - pricingPlan.interval === undefined || - pricingPlan.interval === pricingInterval - ) -} - -const pricingIntervalToLabelMap: Record = { - day: 'daily', - week: 'weekly', - month: 'monthly', - year: 'yearly' -} - -export function getLabelForPricingInterval( - pricingInterval: PricingInterval -): string { - return pricingIntervalToLabelMap[pricingInterval] -} diff --git a/apps/api/src/lib/auth/upsert-or-link-user-account.ts b/apps/api/src/lib/auth/upsert-or-link-user-account.ts index 0cfdaae7..abf5a333 100644 --- a/apps/api/src/lib/auth/upsert-or-link-user-account.ts +++ b/apps/api/src/lib/auth/upsert-or-link-user-account.ts @@ -38,7 +38,10 @@ export async function upsertOrLinkUserAccount({ partialUser: Simplify< SetRequired< Partial< - Pick + Pick< + RawUser, + 'email' | 'name' | 'username' | 'image' | 'isEmailVerified' + > >, 'email' > diff --git a/apps/api/src/lib/billing/upsert-stripe-pricing.ts b/apps/api/src/lib/billing/upsert-stripe-pricing.ts index aec41ede..71526909 100644 --- a/apps/api/src/lib/billing/upsert-stripe-pricing.ts +++ b/apps/api/src/lib/billing/upsert-stripe-pricing.ts @@ -1,17 +1,16 @@ -import type { - PricingPlan, - PricingPlanLineItem -} from '@agentic/platform-schemas' import type Stripe from 'stripe' import { assert } from '@agentic/platform-core' +import { + getLabelForPricingInterval, + type PricingPlan, + type PricingPlanLineItem +} from '@agentic/platform-schemas' import pAll from 'p-all' import { db, eq, - getLabelForPricingInterval, getPricingPlanLineItemHashForStripePrice, - getPricingPlansByInterval, type RawDeployment, type RawProject, schema @@ -28,8 +27,12 @@ import { stripe } from '@/lib/external/stripe' * `_stripeMeterIdMap`, and `_stripePriceIdMap` fields of the given `project`. * * The `project` will be updated in the DB with any changes. + * * The `deployment` is readonly and will not be updated, since all Stripe * resources persist on its Project in case they're the same across deployments. + * + * @note This function assumes that the deployment's pricing config has already + * been validated. */ export async function upsertStripePricing({ deployment, @@ -244,25 +247,6 @@ export async function upsertStripePricing({ } const upserts: Array<() => Promise> = [] - - // Validate deployment pricing plans to ensure they contain at least one valid - // plan per pricing interval configured on the project. - // TODO: move some of this `pricingPlans` validation to a separate function? - // We really wouldn't want to create some resources and then fail partway when - // this validation or some of the validation above fails. - for (const pricingInterval of project.pricingIntervals) { - const pricingPlans = getPricingPlansByInterval({ - pricingInterval, - pricingPlans: deployment.pricingPlans - }) - - assert( - pricingPlans.length > 0, - 400, - `Invalid pricing config for deployment "${deployment.id}": no pricing plans for interval "${pricingInterval}"` - ) - } - for (const pricingPlan of deployment.pricingPlans) { for (const pricingPlanLineItem of pricingPlan.lineItems) { upserts.push(() => diff --git a/apps/api/src/lib/billing/upsert-stripe-subscription.ts b/apps/api/src/lib/billing/upsert-stripe-subscription.ts index 9f1b9185..6da0cc2b 100644 --- a/apps/api/src/lib/billing/upsert-stripe-subscription.ts +++ b/apps/api/src/lib/billing/upsert-stripe-subscription.ts @@ -119,7 +119,7 @@ export async function upsertStripeSubscription( assert( priceId, 500, - `Error updating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line item "${lineItem.slug}"` + `Error updating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line-item "${lineItem.slug}"` ) // An existing Stripe Subscription Item may or may not exist for this @@ -199,9 +199,10 @@ export async function upsertStripeSubscription( updateParams.cancel_at_period_end = true } - if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { - updateParams.application_fee_percent = project.applicationFeePercent - } + // TODO: Stripe Connect + // if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { + // updateParams.application_fee_percent = project.applicationFeePercent + // } subscription = await stripe.subscriptions.update( consumer._stripeSubscriptionId, @@ -274,9 +275,10 @@ export async function upsertStripeSubscription( createParams.trial_period_days = pricingPlan.trialPeriodDays } - if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { - createParams.application_fee_percent = project.applicationFeePercent - } + // TODO: Stripe Connect + // if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { + // createParams.application_fee_percent = project.applicationFeePercent + // } logger.debug('subscription', action, { items }) subscription = await stripe.subscriptions.create( @@ -321,7 +323,7 @@ export async function upsertStripeSubscription( assert( stripeSubscriptionItem, 500, - `Error post-processing stripe subscription for line item "${lineItem.slug}" on plan "${pricingPlan.slug}"` + `Error post-processing stripe subscription for line-item "${lineItem.slug}" on plan "${pricingPlan.slug}"` ) consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug] = @@ -329,7 +331,7 @@ export async function upsertStripeSubscription( assert( consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug], 500, - `Error post-processing stripe subscription for line item "${lineItem.slug}" on plan "${pricingPlan.slug}"` + `Error post-processing stripe subscription for line-item "${lineItem.slug}" on plan "${pricingPlan.slug}"` ) } } diff --git a/packages/api-client/src/openapi.d.ts b/packages/api-client/src/openapi.d.ts index fab56d16..c6f9feec 100644 --- a/packages/api-client/src/openapi.d.ts +++ b/packages/api-client/src/openapi.d.ts @@ -320,9 +320,8 @@ export interface components { role: "user" | "admin"; name?: string; email: string; - emailVerified: boolean; + isEmailVerified: boolean; image?: string; - isStripeConnectEnabledByDefault: boolean; stripeCustomerId?: string; }; Team: { @@ -367,8 +366,6 @@ export interface components { /** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */ lastDeploymentId?: string; applicationFeePercent: number; - isStripeConnectEnabled: boolean; - pricingIntervals: components["schemas"]["PricingInterval"][]; defaultPricingInterval: components["schemas"]["PricingInterval"]; /** @enum {string} */ pricingCurrency: "usd"; @@ -419,7 +416,9 @@ export interface components { /** @example API calls */ label: string; RateLimit: { - interval: number; + /** @description The interval at which the rate limit is applied. Either a number in seconds or a valid [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "1h", "1d", "1w", "1y", etc). */ + interval: number | string; + /** @description Maximum number of operations per interval (unitless). */ maxPerInterval: number; }; PricingPlanTier: { @@ -427,17 +426,15 @@ export interface components { flatAmount?: number; upTo: number | "inf"; }; - /** @description PricingPlanLineItems represent a single line-item in a Stripe Subscription. They map to a Stripe billing `Price` and possibly a corresponding Stripe `Meter` for metered usage. */ + /** @description PricingPlanLineItems represent a single line-item in a Stripe Subscription. They map to a Stripe billing `Price` and possibly a corresponding Stripe `Meter` for usage-based line-items. */ PricingPlanLineItem: { slug: string | "base" | "requests"; - interval?: components["schemas"]["PricingInterval"]; label?: components["schemas"]["label"]; /** @enum {string} */ usageType: "licensed"; amount: number; } | { slug: string | "base" | "requests"; - interval?: components["schemas"]["PricingInterval"]; label?: components["schemas"]["label"]; /** @enum {string} */ usageType: "metered"; @@ -461,11 +458,13 @@ export interface components { name: components["schemas"]["name"]; slug: components["schemas"]["slug"]; interval?: components["schemas"]["PricingInterval"]; - desc?: string; + description?: string; features?: string[]; trialPeriodDays?: number; lineItems: components["schemas"]["PricingPlanLineItem"][]; }; + /** @description List of billing intervals for subscriptions. */ + PricingIntervalList: components["schemas"]["PricingInterval"][]; Deployment: { /** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */ id: string; @@ -489,6 +488,7 @@ export interface components { originAdapter: components["schemas"]["DeploymentOriginAdapter"]; /** @description List of PricingPlans */ pricingPlans: components["schemas"]["PricingPlan"][]; + pricingIntervals: components["schemas"]["PricingIntervalList"]; }; }; responses: { @@ -608,7 +608,6 @@ export interface operations { "application/json": { name?: string; image?: string; - isStripeConnectEnabledByDefault?: boolean; }; }; }; @@ -1270,27 +1269,50 @@ export interface operations { identifier: string; version?: string; published?: boolean; + /** @description A short description of the project. */ description?: string; + /** @description A readme documenting the project (supports GitHub-flavored markdown). */ readme?: string; /** * Format: uri - * @description Logo image URL to use for this deployment. Logos should have a square aspect ratio. + * @description Optional logo image URL to use for the project. Logos should have a square aspect ratio. */ iconUrl?: string; - /** Format: uri */ + /** + * Format: uri + * @description Optional URL to the source code of the project (eg, GitHub repo). + */ sourceUrl?: string; /** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */ projectId: string; /** * Format: uri - * @description Base URL of the externally hosted origin API server. + * @description Required base URL of the externally hosted origin API server. Must be a valid `https` URL. * * NOTE: Agentic currently only supports `external` API servers. If you'd like to host your API or MCP server on Agentic's infrastructure, please reach out to support@agentic.so. */ originUrl: string; originAdapter?: components["schemas"]["DeploymentOriginAdapter"] & unknown; - /** @description List of PricingPlans should be available as subscriptions for this deployment. */ - pricingPlans: components["schemas"]["PricingPlan"][]; + /** + * @description List of PricingPlans configuring which Stripe subscriptions should be available for the project. Defaults to a single free plan which is useful for developing and testing.your project. + * @default [ + * { + * "name": "Free", + * "slug": "free", + * "lineItems": [ + * { + * "slug": "base", + * "usageType": "licensed", + * "amount": 0 + * } + * ] + * } + * ] + */ + pricingPlans?: components["schemas"]["PricingPlan"][]; + pricingIntervals?: components["schemas"]["PricingIntervalList"] & unknown; + /** @description Name of the project. */ + name: string; }; }; }; diff --git a/packages/schemas/src/agentic-project-config-schema.ts b/packages/schemas/src/agentic-project-config-schema.ts index bb27c333..a1fc263a 100644 --- a/packages/schemas/src/agentic-project-config-schema.ts +++ b/packages/schemas/src/agentic-project-config-schema.ts @@ -2,7 +2,7 @@ import { z } from '@hono/zod-openapi' import { deploymentOriginAdapterSchema, - pricingIntervalSchema, + pricingIntervalListSchema, type PricingPlan, pricingPlanListSchema } from './schemas' @@ -54,13 +54,6 @@ export const agenticProjectConfigSchema = z.object({ ) .optional(), - /** Optional URL to the source code of the project. */ - sourceUrl: z - .string() - .url() - .optional() - .describe('Optional URL to the source code of the project.'), - /** * Optional logo image URL to use for the project. Logos should have a square aspect ratio. */ @@ -72,6 +65,15 @@ export const agenticProjectConfigSchema = z.object({ 'Optional logo image URL to use for the project. Logos should have a square aspect ratio.' ), + /** Optional URL to the source code of the project. */ + sourceUrl: z + .string() + .url() + .optional() + .describe( + 'Optional URL to the source code of the project (eg, GitHub repo).' + ), + /** Required origin API HTTPS base URL */ originUrl: z.string().url() .describe(`Required base URL of the externally hosted origin API server. Must be a valid \`https\` URL. @@ -97,7 +99,8 @@ NOTE: Agentic currently only supports \`external\` API servers. If you'd like to * * Defaults to a single monthly interval `['month']`. * - * To add support for annual pricing plans, you can use `['month', 'year']`. + * To add support for annual pricing plans, for example, you can use: + * `['month', 'year']`. * * Note that for every pricing interval, you must define a corresponding set * of PricingPlans in the `pricingPlans` array. If you only have one pricing @@ -106,11 +109,14 @@ NOTE: Agentic currently only supports \`external\` API servers. If you'd like to * specify their `interval` property to differentiate between different * pricing intervals. */ - pricingIntervals: z - .array(pricingIntervalSchema) - .nonempty({ - message: 'Must contain at least one pricing interval' - }) + pricingIntervals: pricingIntervalListSchema + .describe( + `Optional list of billing intervals to enable in the pricingPlans. + +Defaults to a single monthly interval \`['month']\`. + +To add support for annual pricing plans, for example, you can use: \`['month', 'year']\`.` + ) .optional() .default(['month']) }) diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index e789540a..191fc533 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -1,5 +1,6 @@ export * from './agentic-project-config-schema' export * from './define-config' export * from './schemas' +export * from './utils' export * from './validate-agentic-project-config' export * from './validate-origin-adapter' diff --git a/packages/schemas/src/schemas.ts b/packages/schemas/src/schemas.ts index 2d89166d..cffff7a6 100644 --- a/packages/schemas/src/schemas.ts +++ b/packages/schemas/src/schemas.ts @@ -18,19 +18,33 @@ export const rateLimitSchema = z * The interval at which the rate limit is applied. * * Either a number in seconds or a valid [ms](https://github.com/vercel/ms) - * string (eg, `10s`, `1m`, `1h`, `1d`, `1w`, `1y`, etc). + * string (eg, "10s", "1m", "1h", "1d", "1w", "1y", etc). */ - interval: z.union([ - z.number().nonnegative(), // seconds - z - .string() - .nonempty() - .transform((value, ctx) => { - try { - // TODO: `ms` module has broken types - const ms = parseIntervalAsMs(value as any) as unknown as number + interval: z + .union([ + z.number().nonnegative(), // seconds - if (typeof ms !== 'number' || ms < 0) { + z + .string() + .nonempty() + .transform((value, ctx) => { + try { + // TODO: `ms` module has broken types + const ms = parseIntervalAsMs(value as any) as unknown as number + + if (typeof ms !== 'number' || ms < 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid interval "${value}"`, + path: ctx.path + }) + + return z.NEVER + } + + const seconds = Math.floor(ms / 1000) + return seconds + } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid interval "${value}"`, @@ -39,20 +53,11 @@ export const rateLimitSchema = z return z.NEVER } - - const seconds = Math.floor(ms / 1000) - return seconds - } catch { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Invalid interval "${value}"`, - path: ctx.path - }) - - return z.NEVER - } - }) - ]), + }) + ]) + .describe( + `The interval at which the rate limit is applied. Either a number in seconds or a valid [ms](https://github.com/vercel/ms) string (eg, "10s", "1m", "1h", "1d", "1w", "1y", etc).` + ), /** * Maximum number of operations per interval (unitless). @@ -99,6 +104,17 @@ export const pricingIntervalSchema = z .openapi('PricingInterval') export type PricingInterval = z.infer +/** + * List of billing intervals for subscriptions. + */ +export const pricingIntervalListSchema = z + .array(pricingIntervalSchema) + .nonempty({ + message: 'Must contain at least one pricing interval' + }) + .describe('List of billing intervals for subscriptions.') + .openapi('PricingIntervalList') + /** * Internal PricingPlanLineItem hash * diff --git a/packages/schemas/src/utils.ts b/packages/schemas/src/utils.ts new file mode 100644 index 00000000..abcf9fac --- /dev/null +++ b/packages/schemas/src/utils.ts @@ -0,0 +1,28 @@ +import type { PricingInterval, PricingPlan, PricingPlanList } from './schemas' + +export function getPricingPlansByInterval({ + pricingInterval, + pricingPlans +}: { + pricingInterval: PricingInterval + pricingPlans: PricingPlanList +}): PricingPlan[] { + return pricingPlans.filter( + (pricingPlan) => + pricingPlan.interval === pricingInterval || + pricingPlan.interval === undefined + ) +} + +const pricingIntervalToLabelMap: Record = { + day: 'daily', + week: 'weekly', + month: 'monthly', + year: 'yearly' +} + +export function getLabelForPricingInterval( + pricingInterval: PricingInterval +): string { + return pricingIntervalToLabelMap[pricingInterval] +} diff --git a/packages/schemas/src/validate-agentic-project-config.ts b/packages/schemas/src/validate-agentic-project-config.ts index d5b0e9dd..7a43a426 100644 --- a/packages/schemas/src/validate-agentic-project-config.ts +++ b/packages/schemas/src/validate-agentic-project-config.ts @@ -8,6 +8,7 @@ import { type AgenticProjectConfigInput, agenticProjectConfigSchema } from './agentic-project-config-schema' +import { getPricingPlansByInterval } from './utils' import { validateOriginAdapter } from './validate-origin-adapter' export async function validateAgenticProjectConfig( @@ -199,6 +200,21 @@ export async function validateAgenticProjectConfig( } } + // Validate deployment pricing plans to ensure they contain at least one valid + // plan per pricing interval configured on the project. + for (const pricingInterval of config.pricingIntervals) { + const pricingPlansForInterval = getPricingPlansByInterval({ + pricingInterval, + pricingPlans + }) + + assert( + pricingPlansForInterval.length > 0, + 400, + `Invalid pricing config: no pricing plans for pricing interval "${pricingInterval}"` + ) + } + await validateOriginAdapter({ ...opts, label: `project "${name}"`, diff --git a/readme.md b/readme.md index a030e010..41c6d9e3 100644 --- a/readme.md +++ b/readme.md @@ -7,17 +7,24 @@ ## TODO +- end-to-end working examples + - raw + - openapi + - mcp - stripe - re-add coupons - declarative json-based pricing - like https://github.com/tierrun/tier and Saasify - https://github.com/tierrun/tier/blob/main/pricing/schema.json - https://blog.tier.run/tier-hello-world-demo + - stripe connect - consider switching to [consola](https://github.com/unjs/consola) for logging? - consider switching to `bun` (for `--hot` reloading!!) - transactional emails - openauth password emails and `sendCode` - re-add support for teams / organizations +- api gateway + - signed requests ## License