pull/715/head
Travis Fischer 2025-05-26 16:36:44 +07:00
rodzic 6f35cffae9
commit 8b039708ff
15 zmienionych plików z 205 dodań i 170 usunięć

Wyświetl plik

@ -104,7 +104,7 @@ export const authRouter = issuer({
}, },
partialUser: { partialUser: {
email: ghUser.email, email: ghUser.email,
emailVerified: true, isEmailVerified: true,
name: ghUser.name || undefined, name: ghUser.name || undefined,
username: ghUser.login.toLowerCase(), username: ghUser.login.toLowerCase(),
image: ghUser.avatar_url image: ghUser.avatar_url
@ -118,7 +118,7 @@ export const authRouter = issuer({
}, },
partialUser: { partialUser: {
email: value.email, email: value.email,
emailVerified: true isEmailVerified: true
} }
}) })
} else { } else {

Wyświetl plik

@ -1,6 +1,8 @@
import { import {
agenticProjectConfigSchema,
type DeploymentOriginAdapter, type DeploymentOriginAdapter,
deploymentOriginAdapterSchema, deploymentOriginAdapterSchema,
pricingIntervalListSchema,
type PricingPlanList, type PricingPlanList,
pricingPlanListSchema pricingPlanListSchema
} from '@agentic/platform-schemas' } from '@agentic/platform-schemas'
@ -29,6 +31,7 @@ import {
createUpdateSchema, createUpdateSchema,
deploymentIdentifier, deploymentIdentifier,
deploymentPrimaryId, deploymentPrimaryId,
pricingIntervalEnum,
projectId, projectId,
teamId, teamId,
timestamps, timestamps,
@ -65,11 +68,10 @@ export const deployments = pgTable(
onDelete: 'cascade' onDelete: 'cascade'
}), }),
// TODO: Tool definitions or OpenAPI spec // TODO: Tool definitions
// services: jsonb().$type<Service[]>().default([]), // tools: jsonb().$type<Tool[]>().default([]),
// TODO: metadata config (logo, keywords, examples, etc) // TODO: metadata config (logo, keywords, examples, etc)
// TODO: openapi spec or tool definitions or mcp adapter
// TODO: webhooks // TODO: webhooks
// TODO: third-party auth provider config // TODO: third-party auth provider config
// NOTE: will need consumer.authProviders as well as user.authProviders for // NOTE: will need consumer.authProviders as well as user.authProviders for
@ -84,7 +86,10 @@ export const deployments = pgTable(
originAdapter: jsonb().$type<DeploymentOriginAdapter>().notNull(), originAdapter: jsonb().$type<DeploymentOriginAdapter>().notNull(),
// Array<PricingPlan> // Array<PricingPlan>
pricingPlans: jsonb().$type<PricingPlanList>().notNull() pricingPlans: jsonb().$type<PricingPlanList>().notNull(),
// Which pricing intervals are supported for subscriptions to this project
pricingIntervals: pricingIntervalEnum().array().default(['month']).notNull()
// coupons: jsonb().$type<Coupon[]>().default([]).notNull() // coupons: jsonb().$type<Coupon[]>().default([]).notNull()
}, },
@ -135,8 +140,9 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
message: 'Invalid deployment hash' message: 'Invalid deployment hash'
}), }),
originAdapter: deploymentOriginAdapterSchema,
pricingPlans: pricingPlanListSchema, pricingPlans: pricingPlanListSchema,
originAdapter: deploymentOriginAdapterSchema pricingIntervals: pricingIntervalListSchema
}) })
.omit({ .omit({
originUrl: true originUrl: true
@ -159,33 +165,7 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
.openapi('Deployment') .openapi('Deployment')
export const deploymentInsertSchema = createInsertSchema(deployments, { export const deploymentInsertSchema = createInsertSchema(deployments, {
projectId: projectIdSchema, 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()
}) })
.omit({ .omit({
id: true, id: true,
@ -195,8 +175,15 @@ NOTE: Agentic currently only supports \`external\` API servers. If you'd like to
userId: true, userId: true,
teamId: true teamId: true
}) })
.extend({
...agenticProjectConfigSchema.shape
})
.strict() .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) export const deploymentUpdateSchema = createUpdateSchema(deployments)
.pick({ .pick({
deletedAt: true, deletedAt: true,

Wyświetl plik

@ -10,7 +10,6 @@ import {
import { validators } from '@agentic/platform-validators' import { validators } from '@agentic/platform-validators'
import { relations } from '@fisch0920/drizzle-orm' import { relations } from '@fisch0920/drizzle-orm'
import { import {
boolean,
index, index,
integer, integer,
jsonb, jsonb,
@ -18,7 +17,6 @@ import {
text, text,
uniqueIndex uniqueIndex
} from '@fisch0920/drizzle-orm/pg-core' } from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import { import {
deploymentIdSchema, deploymentIdSchema,
@ -69,21 +67,18 @@ export const projects = pgTable(
applicationFeePercent: integer().default(20).notNull(), applicationFeePercent: integer().default(20).notNull(),
// TODO: This is going to need to vary from dev to prod // TODO: This is going to need to vary from dev to prod
isStripeConnectEnabled: boolean().default(false).notNull(), //isStripeConnectEnabled: boolean().default(false).notNull(),
// Which pricing intervals are supported for subscriptions to this project
pricingIntervals: pricingIntervalEnum()
.array()
.default(['month'])
.notNull(),
// Default pricing interval for subscriptions to this project // 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(), defaultPricingInterval: pricingIntervalEnum().default('month').notNull(),
// Pricing currency used across all prices and subscriptions to this project // Pricing currency used across all prices and subscriptions to this project
pricingCurrency: pricingCurrencyEnum().default('usd').notNull(), 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(), _secret: text().notNull(),
// Auth token used to access the platform API on behalf of this project // 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(), applicationFeePercent: (schema) => schema.nonnegative(),
pricingIntervals: z.array(pricingIntervalSchema).nonempty(),
defaultPricingInterval: pricingIntervalSchema, defaultPricingInterval: pricingIntervalSchema,
_stripeProductIdMap: stripeProductIdMapSchema, _stripeProductIdMap: stripeProductIdMapSchema,

Wyświetl plik

@ -19,8 +19,6 @@ import {
userRoleEnum userRoleEnum
} from './common' } from './common'
// This table is mostly managed by better-auth.
export const users = pgTable( export const users = pgTable(
'users', 'users',
{ {
@ -32,10 +30,10 @@ export const users = pgTable(
name: text(), name: text(),
email: text().notNull().unique(), email: text().notNull().unique(),
emailVerified: boolean().default(false).notNull(), isEmailVerified: boolean().default(false).notNull(),
image: text(), image: text(),
isStripeConnectEnabledByDefault: boolean().default(true).notNull(), //isStripeConnectEnabledByDefault: boolean().default(true).notNull(),
stripeCustomerId: stripeId() stripeCustomerId: stripeId()
}, },
@ -59,7 +57,7 @@ export const userSelectSchema = createSelectSchema(users)
export const userUpdateSchema = createUpdateSchema(users) export const userUpdateSchema = createUpdateSchema(users)
.pick({ .pick({
name: true, name: true,
image: true, image: true
isStripeConnectEnabledByDefault: true //isStripeConnectEnabledByDefault: true
}) })
.strict() .strict()

Wyświetl plik

@ -1,8 +1,6 @@
import type { import type {
PricingInterval,
PricingPlan, PricingPlan,
PricingPlanLineItem, PricingPlanLineItem
PricingPlanList
} from '@agentic/platform-schemas' } from '@agentic/platform-schemas'
import { hashObject } from '@agentic/platform-core' import { hashObject } from '@agentic/platform-core'
@ -62,30 +60,3 @@ export function getStripePriceIdForPricingPlanLineItem({
return project._stripePriceIdMap[pricingPlanLineItemHash] 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<PricingInterval, string> = {
day: 'daily',
week: 'weekly',
month: 'monthly',
year: 'yearly'
}
export function getLabelForPricingInterval(
pricingInterval: PricingInterval
): string {
return pricingIntervalToLabelMap[pricingInterval]
}

Wyświetl plik

@ -38,7 +38,10 @@ export async function upsertOrLinkUserAccount({
partialUser: Simplify< partialUser: Simplify<
SetRequired< SetRequired<
Partial< Partial<
Pick<RawUser, 'email' | 'name' | 'username' | 'image' | 'emailVerified'> Pick<
RawUser,
'email' | 'name' | 'username' | 'image' | 'isEmailVerified'
>
>, >,
'email' 'email'
> >

Wyświetl plik

@ -1,17 +1,16 @@
import type {
PricingPlan,
PricingPlanLineItem
} from '@agentic/platform-schemas'
import type Stripe from 'stripe' import type Stripe from 'stripe'
import { assert } from '@agentic/platform-core' import { assert } from '@agentic/platform-core'
import {
getLabelForPricingInterval,
type PricingPlan,
type PricingPlanLineItem
} from '@agentic/platform-schemas'
import pAll from 'p-all' import pAll from 'p-all'
import { import {
db, db,
eq, eq,
getLabelForPricingInterval,
getPricingPlanLineItemHashForStripePrice, getPricingPlanLineItemHashForStripePrice,
getPricingPlansByInterval,
type RawDeployment, type RawDeployment,
type RawProject, type RawProject,
schema schema
@ -28,8 +27,12 @@ import { stripe } from '@/lib/external/stripe'
* `_stripeMeterIdMap`, and `_stripePriceIdMap` fields of the given `project`. * `_stripeMeterIdMap`, and `_stripePriceIdMap` fields of the given `project`.
* *
* The `project` will be updated in the DB with any changes. * The `project` will be updated in the DB with any changes.
*
* The `deployment` is readonly and will not be updated, since all Stripe * 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. * 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({ export async function upsertStripePricing({
deployment, deployment,
@ -244,25 +247,6 @@ export async function upsertStripePricing({
} }
const upserts: Array<() => Promise<void>> = [] const upserts: Array<() => Promise<void>> = []
// 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 pricingPlan of deployment.pricingPlans) {
for (const pricingPlanLineItem of pricingPlan.lineItems) { for (const pricingPlanLineItem of pricingPlan.lineItems) {
upserts.push(() => upserts.push(() =>

Wyświetl plik

@ -119,7 +119,7 @@ export async function upsertStripeSubscription(
assert( assert(
priceId, priceId,
500, 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 // 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 updateParams.cancel_at_period_end = true
} }
if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { // TODO: Stripe Connect
updateParams.application_fee_percent = project.applicationFeePercent // if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) {
} // updateParams.application_fee_percent = project.applicationFeePercent
// }
subscription = await stripe.subscriptions.update( subscription = await stripe.subscriptions.update(
consumer._stripeSubscriptionId, consumer._stripeSubscriptionId,
@ -274,9 +275,10 @@ export async function upsertStripeSubscription(
createParams.trial_period_days = pricingPlan.trialPeriodDays createParams.trial_period_days = pricingPlan.trialPeriodDays
} }
if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { // TODO: Stripe Connect
createParams.application_fee_percent = project.applicationFeePercent // if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) {
} // createParams.application_fee_percent = project.applicationFeePercent
// }
logger.debug('subscription', action, { items }) logger.debug('subscription', action, { items })
subscription = await stripe.subscriptions.create( subscription = await stripe.subscriptions.create(
@ -321,7 +323,7 @@ export async function upsertStripeSubscription(
assert( assert(
stripeSubscriptionItem, stripeSubscriptionItem,
500, 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] = consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug] =
@ -329,7 +331,7 @@ export async function upsertStripeSubscription(
assert( assert(
consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug], consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug],
500, 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}"`
) )
} }
} }

Wyświetl plik

@ -320,9 +320,8 @@ export interface components {
role: "user" | "admin"; role: "user" | "admin";
name?: string; name?: string;
email: string; email: string;
emailVerified: boolean; isEmailVerified: boolean;
image?: string; image?: string;
isStripeConnectEnabledByDefault: boolean;
stripeCustomerId?: string; stripeCustomerId?: string;
}; };
Team: { Team: {
@ -367,8 +366,6 @@ export interface components {
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */ /** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
lastDeploymentId?: string; lastDeploymentId?: string;
applicationFeePercent: number; applicationFeePercent: number;
isStripeConnectEnabled: boolean;
pricingIntervals: components["schemas"]["PricingInterval"][];
defaultPricingInterval: components["schemas"]["PricingInterval"]; defaultPricingInterval: components["schemas"]["PricingInterval"];
/** @enum {string} */ /** @enum {string} */
pricingCurrency: "usd"; pricingCurrency: "usd";
@ -419,7 +416,9 @@ export interface components {
/** @example API calls */ /** @example API calls */
label: string; label: string;
RateLimit: { 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; maxPerInterval: number;
}; };
PricingPlanTier: { PricingPlanTier: {
@ -427,17 +426,15 @@ export interface components {
flatAmount?: number; flatAmount?: number;
upTo: number | "inf"; 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: { PricingPlanLineItem: {
slug: string | "base" | "requests"; slug: string | "base" | "requests";
interval?: components["schemas"]["PricingInterval"];
label?: components["schemas"]["label"]; label?: components["schemas"]["label"];
/** @enum {string} */ /** @enum {string} */
usageType: "licensed"; usageType: "licensed";
amount: number; amount: number;
} | { } | {
slug: string | "base" | "requests"; slug: string | "base" | "requests";
interval?: components["schemas"]["PricingInterval"];
label?: components["schemas"]["label"]; label?: components["schemas"]["label"];
/** @enum {string} */ /** @enum {string} */
usageType: "metered"; usageType: "metered";
@ -461,11 +458,13 @@ export interface components {
name: components["schemas"]["name"]; name: components["schemas"]["name"];
slug: components["schemas"]["slug"]; slug: components["schemas"]["slug"];
interval?: components["schemas"]["PricingInterval"]; interval?: components["schemas"]["PricingInterval"];
desc?: string; description?: string;
features?: string[]; features?: string[];
trialPeriodDays?: number; trialPeriodDays?: number;
lineItems: components["schemas"]["PricingPlanLineItem"][]; lineItems: components["schemas"]["PricingPlanLineItem"][];
}; };
/** @description List of billing intervals for subscriptions. */
PricingIntervalList: components["schemas"]["PricingInterval"][];
Deployment: { Deployment: {
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */ /** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
id: string; id: string;
@ -489,6 +488,7 @@ export interface components {
originAdapter: components["schemas"]["DeploymentOriginAdapter"]; originAdapter: components["schemas"]["DeploymentOriginAdapter"];
/** @description List of PricingPlans */ /** @description List of PricingPlans */
pricingPlans: components["schemas"]["PricingPlan"][]; pricingPlans: components["schemas"]["PricingPlan"][];
pricingIntervals: components["schemas"]["PricingIntervalList"];
}; };
}; };
responses: { responses: {
@ -608,7 +608,6 @@ export interface operations {
"application/json": { "application/json": {
name?: string; name?: string;
image?: string; image?: string;
isStripeConnectEnabledByDefault?: boolean;
}; };
}; };
}; };
@ -1270,27 +1269,50 @@ export interface operations {
identifier: string; identifier: string;
version?: string; version?: string;
published?: boolean; published?: boolean;
/** @description A short description of the project. */
description?: string; description?: string;
/** @description A readme documenting the project (supports GitHub-flavored markdown). */
readme?: string; readme?: string;
/** /**
* Format: uri * 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; iconUrl?: string;
/** Format: uri */ /**
* Format: uri
* @description Optional URL to the source code of the project (eg, GitHub repo).
*/
sourceUrl?: string; sourceUrl?: string;
/** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */ /** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */
projectId: string; projectId: string;
/** /**
* Format: uri * 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. * 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; originUrl: string;
originAdapter?: components["schemas"]["DeploymentOriginAdapter"] & unknown; 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;
}; };
}; };
}; };

Wyświetl plik

@ -2,7 +2,7 @@ import { z } from '@hono/zod-openapi'
import { import {
deploymentOriginAdapterSchema, deploymentOriginAdapterSchema,
pricingIntervalSchema, pricingIntervalListSchema,
type PricingPlan, type PricingPlan,
pricingPlanListSchema pricingPlanListSchema
} from './schemas' } from './schemas'
@ -54,13 +54,6 @@ export const agenticProjectConfigSchema = z.object({
) )
.optional(), .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. * 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 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 */ /** Required origin API HTTPS base URL */
originUrl: z.string().url() originUrl: z.string().url()
.describe(`Required base URL of the externally hosted origin API server. Must be a valid \`https\` 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']`. * 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 * Note that for every pricing interval, you must define a corresponding set
* of PricingPlans in the `pricingPlans` array. If you only have one pricing * 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 * specify their `interval` property to differentiate between different
* pricing intervals. * pricing intervals.
*/ */
pricingIntervals: z pricingIntervals: pricingIntervalListSchema
.array(pricingIntervalSchema) .describe(
.nonempty({ `Optional list of billing intervals to enable in the pricingPlans.
message: 'Must contain at least one pricing interval'
}) Defaults to a single monthly interval \`['month']\`.
To add support for annual pricing plans, for example, you can use: \`['month', 'year']\`.`
)
.optional() .optional()
.default(['month']) .default(['month'])
}) })

Wyświetl plik

@ -1,5 +1,6 @@
export * from './agentic-project-config-schema' export * from './agentic-project-config-schema'
export * from './define-config' export * from './define-config'
export * from './schemas' export * from './schemas'
export * from './utils'
export * from './validate-agentic-project-config' export * from './validate-agentic-project-config'
export * from './validate-origin-adapter' export * from './validate-origin-adapter'

Wyświetl plik

@ -18,19 +18,33 @@ export const rateLimitSchema = z
* The interval at which the rate limit is applied. * The interval at which the rate limit is applied.
* *
* Either a number in seconds or a valid [ms](https://github.com/vercel/ms) * 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([ interval: z
z.number().nonnegative(), // seconds .union([
z z.number().nonnegative(), // seconds
.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) { 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({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: `Invalid interval "${value}"`, message: `Invalid interval "${value}"`,
@ -39,20 +53,11 @@ export const rateLimitSchema = z
return z.NEVER return z.NEVER
} }
})
const seconds = Math.floor(ms / 1000) ])
return seconds .describe(
} catch { `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).`
ctx.addIssue({ ),
code: z.ZodIssueCode.custom,
message: `Invalid interval "${value}"`,
path: ctx.path
})
return z.NEVER
}
})
]),
/** /**
* Maximum number of operations per interval (unitless). * Maximum number of operations per interval (unitless).
@ -99,6 +104,17 @@ export const pricingIntervalSchema = z
.openapi('PricingInterval') .openapi('PricingInterval')
export type PricingInterval = z.infer<typeof pricingIntervalSchema> export type PricingInterval = z.infer<typeof pricingIntervalSchema>
/**
* 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 * Internal PricingPlanLineItem hash
* *

Wyświetl plik

@ -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<PricingInterval, string> = {
day: 'daily',
week: 'weekly',
month: 'monthly',
year: 'yearly'
}
export function getLabelForPricingInterval(
pricingInterval: PricingInterval
): string {
return pricingIntervalToLabelMap[pricingInterval]
}

Wyświetl plik

@ -8,6 +8,7 @@ import {
type AgenticProjectConfigInput, type AgenticProjectConfigInput,
agenticProjectConfigSchema agenticProjectConfigSchema
} from './agentic-project-config-schema' } from './agentic-project-config-schema'
import { getPricingPlansByInterval } from './utils'
import { validateOriginAdapter } from './validate-origin-adapter' import { validateOriginAdapter } from './validate-origin-adapter'
export async function validateAgenticProjectConfig( 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({ await validateOriginAdapter({
...opts, ...opts,
label: `project "${name}"`, label: `project "${name}"`,

Wyświetl plik

@ -7,17 +7,24 @@
## TODO ## TODO
- end-to-end working examples
- raw
- openapi
- mcp
- stripe - stripe
- re-add coupons - re-add coupons
- declarative json-based pricing - declarative json-based pricing
- like https://github.com/tierrun/tier and Saasify - like https://github.com/tierrun/tier and Saasify
- https://github.com/tierrun/tier/blob/main/pricing/schema.json - https://github.com/tierrun/tier/blob/main/pricing/schema.json
- https://blog.tier.run/tier-hello-world-demo - https://blog.tier.run/tier-hello-world-demo
- stripe connect
- consider switching to [consola](https://github.com/unjs/consola) for logging? - consider switching to [consola](https://github.com/unjs/consola) for logging?
- consider switching to `bun` (for `--hot` reloading!!) - consider switching to `bun` (for `--hot` reloading!!)
- transactional emails - transactional emails
- openauth password emails and `sendCode` - openauth password emails and `sendCode`
- re-add support for teams / organizations - re-add support for teams / organizations
- api gateway
- signed requests
## License ## License