diff --git a/apps/api/src/api-v1/deployments/create-deployment.ts b/apps/api/src/api-v1/deployments/create-deployment.ts index aee6542c..16604bf1 100644 --- a/apps/api/src/api-v1/deployments/create-deployment.ts +++ b/apps/api/src/api-v1/deployments/create-deployment.ts @@ -1,5 +1,5 @@ -import { assert, parseZodSchema, pick, sha256 } from '@agentic/platform-core' -import { validateOriginAdapter } from '@agentic/platform-schemas' +import { assert, parseZodSchema, sha256 } from '@agentic/platform-core' +import { validateAgenticProjectConfig } from '@agentic/platform-schemas' import { validators } from '@agentic/platform-validators' import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' @@ -99,10 +99,15 @@ export function registerV1DeploymentsCreateDeployment( }) } - // Validate origin config, including any OpenAPI or MCP specs - await validateOriginAdapter({ - ...pick(body, 'originUrl', 'originAdapter'), + // Validate project config, including: + // - pricing plans + // - origin adapter config + // - origin API base UrL + // - origin adapter OpenAPI or MCP specs + // - tool definitions + const agenticProjectConfig = await validateAgenticProjectConfig(body, { label: `deployment "${deploymentIdentifier}"`, + strip: true, logger }) @@ -111,6 +116,7 @@ export function registerV1DeploymentsCreateDeployment( .insert(schema.deployments) .values({ ...body, + ...agenticProjectConfig, identifier: deploymentIdentifier, hash, userId: user.id, @@ -139,8 +145,6 @@ export function registerV1DeploymentsCreateDeployment( version: deployment.version! }) } - // TODO: validate deployment originUrl, originAdapter, originSchema, and - // originSchemaVersion return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment)) }) diff --git a/apps/api/src/db/schema/consumer.ts b/apps/api/src/db/schema/consumer.ts index 6ef905b7..30cfc6fa 100644 --- a/apps/api/src/db/schema/consumer.ts +++ b/apps/api/src/db/schema/consumer.ts @@ -38,14 +38,13 @@ import { users } from './user' // https://docs.rapidapi.com/docs/keys#section-different-api-keys-per-application /** - * A `Consumer` is a user who has subscribed to a `Project`. - * - * Consumers are used to track usage and billing for a project. + * A `Consumer` represents a user who has subscribed to a `Project` and is used + * to track usage and billing. * * Consumers are linked to a corresponding Stripe Customer and Subscription. - * The Stripe customer will either be the user's default Stripe Customer for - * the platform account, or a customer on the project's connected Stripe - * account if the project has Stripe Connect enabled. + * The Stripe customer will either be the user's default Stripe Customer if the + * project uses the default Agentic platform account, or a customer on the project + * owner's connected Stripe account if the project has Stripe Connect enabled. */ export const consumers = pgTable( 'consumers', @@ -168,6 +167,15 @@ export const consumerSelectSchema = createSelectSchema(consumers, { // .openapi('Deployment', { type: 'object' }) // }) .strip() + .describe( + `A Consumer represents a user who has subscribed to a Project and is used +to track usage and billing. + +Consumers are linked to a corresponding Stripe Customer and Subscription. +The Stripe customer will either be the user's default Stripe Customer if the +project uses the default Agentic platform account, or a customer on the project +owner's connected Stripe account if the project has Stripe Connect enabled.` + ) .openapi('Consumer') export const consumerInsertSchema = createInsertSchema(consumers, { diff --git a/apps/api/src/db/schema/deployment.ts b/apps/api/src/db/schema/deployment.ts index 8546738e..8cab6c25 100644 --- a/apps/api/src/db/schema/deployment.ts +++ b/apps/api/src/db/schema/deployment.ts @@ -41,6 +41,15 @@ import { projects } from './project' import { teams } from './team' import { users } from './user' +/** + * A Deployment is a single, immutable instance of a Project. Each deployment + * contains pricing plans, origin server config (OpenAPI or MCP server), tool + * definitions, and metadata. + * + * Deployments are private to a developer or team until they are published, at + * which point they are accessible to any customers with access to the parent + * Project. + */ export const deployments = pgTable( 'deployments', { @@ -162,6 +171,11 @@ export const deploymentSelectSchema = createSelectSchema(deployments, { // project: z.object({}).optional().openapi('Project', { type: 'object' }) // }) .strip() + .describe( + `A Deployment is a single, immutable instance of a Project. Each deployment contains pricing plans, origin server config (OpenAPI or MCP server), tool definitions, and metadata. + +Deployments are private to a developer or team until they are published, at which point they are accessible to any customers with access to the parent Project.` + ) .openapi('Deployment') export const deploymentInsertSchema = createInsertSchema(deployments, { diff --git a/apps/api/src/db/schema/project.ts b/apps/api/src/db/schema/project.ts index 02349c46..47fa27da 100644 --- a/apps/api/src/db/schema/project.ts +++ b/apps/api/src/db/schema/project.ts @@ -43,6 +43,18 @@ import { deployments } from './deployment' import { teams } from './team' import { users } from './user' +/** + * A Project represents a single Agentic API product. A Project is comprised of + * a series of immutable Deployments, each of which contains pricing data, origin + * API config, OpenAPI or MCP specs, tool definitions, and various metadata. + * + * You can think of Agentic Projects as similar to Vercel projects. They both + * hold some common configuration and are comprised of a series of immutable + * Deployments. + * + * Internally, Projects manage all of the Stripe billing resources across + * Deployments (Stripe Products, Prices, and Meters for usage-based billing). + */ export const projects = pgTable( 'projects', { @@ -210,6 +222,13 @@ export const projectSelectSchema = createSelectSchema(projects, { // .openapi('Deployment', { type: 'object' }) // }) .strip() + .describe( + `A Project represents a single Agentic API product. A Project is comprised of a series of immutable Deployments, each of which contains pricing data, origin API config, OpenAPI or MCP specs, tool definitions, and various metadata. + +You can think of Agentic Projects as similar to Vercel projects. They both hold some common configuration and are comprised of a series of immutable Deployments. + +Internally, Projects manage all of the Stripe billing resources across Deployments (Stripe Products, Prices, and Meters for usage-based billing).` + ) .openapi('Project') export const projectInsertSchema = createInsertSchema(projects, { diff --git a/packages/schemas/src/agentic-project-config-schema.ts b/packages/schemas/src/agentic-project-config-schema.ts index a1fc263a..9088a723 100644 --- a/packages/schemas/src/agentic-project-config-schema.ts +++ b/packages/schemas/src/agentic-project-config-schema.ts @@ -29,97 +29,99 @@ export const defaultFreePricingPlan = { ] } as const satisfies Readonly -export const agenticProjectConfigSchema = z.object({ - /** - * Required name of the project. - * - * Must be lower kebab-case with no spaces and between 2 and 64 characters. - * - * @example "my-project" - * @example "linkedin-resolver-23" - */ - name: z.string().nonempty().describe('Name of the project.'), +export const agenticProjectConfigSchema = z + .object({ + /** + * Required name of the project. + * + * Must be lower kebab-case with no spaces and between 2 and 64 characters. + * + * @example "my-project" + * @example "linkedin-resolver-23" + */ + name: z.string().nonempty().describe('Name of the project.'), - /** Optional short description of the project. */ - description: z - .string() - .describe('A short description of the project.') - .optional(), + /** Optional short description of the project. */ + description: z + .string() + .describe('A short description of the project.') + .optional(), - /** Optional readme documenting the project (supports GitHub-flavored markdown). */ - readme: z - .string() - .describe( - 'A readme documenting the project (supports GitHub-flavored markdown).' - ) - .optional(), + /** Optional readme documenting the project (supports GitHub-flavored markdown). */ + readme: z + .string() + .describe( + 'A readme documenting the project (supports GitHub-flavored markdown).' + ) + .optional(), - /** - * Optional logo image URL to use for the project. Logos should have a square aspect ratio. - */ - iconUrl: z - .string() - .url() - .optional() - .describe( - 'Optional logo image URL to use for the project. Logos should have a square aspect ratio.' - ), + /** + * Optional logo image URL to use for the project. Logos should have a square aspect ratio. + */ + iconUrl: z + .string() + .url() + .optional() + .describe( + '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).' - ), + /** 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. + /** 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. 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.`), - /** Optional origin API config */ - originAdapter: deploymentOriginAdapterSchema.optional().default({ - location: 'external', - type: 'raw' - }), + /** Optional origin API config */ + originAdapter: deploymentOriginAdapterSchema.optional().default({ + location: 'external', + type: 'raw' + }), - /** Optional subscription pricing config */ - pricingPlans: pricingPlanListSchema - .describe( - '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.' - ) - .optional() - .default([defaultFreePricingPlan]), + /** Optional subscription pricing config */ + pricingPlans: pricingPlanListSchema + .describe( + '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.' + ) + .optional() + .default([defaultFreePricingPlan]), - /** - * 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']`. - * - * Note that for every pricing interval, you must define a corresponding set - * of PricingPlans in the `pricingPlans` array. If you only have one pricing - * interval (like the default `month` interval), `pricingPlans` don't need to - * specify their `interval` property. Otherwise, all PricingPlans must - * specify their `interval` property to differentiate between different - * pricing intervals. - */ - pricingIntervals: pricingIntervalListSchema - .describe( - `Optional list of billing intervals to enable in the pricingPlans. + /** + * 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']`. + * + * Note that for every pricing interval, you must define a corresponding set + * of PricingPlans in the `pricingPlans` array. If you only have one pricing + * interval (like the default `month` interval), `pricingPlans` don't need to + * specify their `interval` property. Otherwise, all PricingPlans must + * specify their `interval` property to differentiate between different + * pricing intervals. + */ + 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']) -}) + ) + .optional() + .default(['month']) + }) + .strip() export type AgenticProjectConfigInput = z.input< typeof agenticProjectConfigSchema diff --git a/packages/schemas/src/validate-agentic-project-config.ts b/packages/schemas/src/validate-agentic-project-config.ts index 7a43a426..2e73a12f 100644 --- a/packages/schemas/src/validate-agentic-project-config.ts +++ b/packages/schemas/src/validate-agentic-project-config.ts @@ -13,13 +13,21 @@ import { validateOriginAdapter } from './validate-origin-adapter' export async function validateAgenticProjectConfig( inputConfig: unknown, - opts: { logger?: Logger; cwd?: URL } = {} + { + strip = false, + ...opts + }: { logger?: Logger; cwd?: URL; strip?: boolean; label?: string } = {} ): Promise { const config = parseZodSchema< AgenticProjectConfig, ZodTypeDef, AgenticProjectConfigInput - >(agenticProjectConfigSchema, inputConfig) + >( + strip + ? agenticProjectConfigSchema.strip() + : agenticProjectConfigSchema.strict(), + inputConfig + ) const { name, pricingIntervals, pricingPlans, originUrl } = config assert( @@ -216,8 +224,8 @@ export async function validateAgenticProjectConfig( } await validateOriginAdapter({ - ...opts, label: `project "${name}"`, + ...opts, originUrl, originAdapter: config.originAdapter })