feat: WIP kittens

pull/715/head
Travis Fischer 2025-05-31 16:29:45 +07:00
rodzic 85646edfdc
commit 80bff87be6
13 zmienionych plików z 185 dodań i 50 usunięć

Wyświetl plik

@ -45,15 +45,23 @@ export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
const { deploymentIdentifier, populate = [] } = c.req.valid('query') const { deploymentIdentifier, populate = [] } = c.req.valid('query')
await aclAdmin(c) await aclAdmin(c)
const deployment = await tryGetDeploymentByIdentifier(c, { const { project, ...deployment } = await tryGetDeploymentByIdentifier(c, {
deploymentIdentifier, deploymentIdentifier,
with: { with: {
...Object.fromEntries(populate.map((field) => [field, true])) ...Object.fromEntries(populate.map((field) => [field, true])),
project: true
} }
}) })
assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`) assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`)
assert(
project,
404,
`Project not found for deployment "${deploymentIdentifier}"`
)
await acl(c, deployment, { label: 'Deployment' }) await acl(c, deployment, { label: 'Deployment' })
const hasPopulateProject = populate.includes('project')
// TODO // TODO
// TODO: switch from published to publishedAt? // TODO: switch from published to publishedAt?
// if (deployment.published) { // if (deployment.published) {
@ -69,7 +77,11 @@ export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
// } // }
return c.json( return c.json(
parseZodSchema(schema.deploymentAdminSelectSchema, deployment) parseZodSchema(schema.deploymentAdminSelectSchema, {
...deployment,
...(hasPopulateProject ? { project } : {}),
_secret: project._secret
})
) )
}) })
} }

Wyświetl plik

@ -88,8 +88,9 @@ export const consumers = pgTable(
onDelete: 'cascade' onDelete: 'cascade'
}), }),
// Stripe subscription status (synced via webhooks) // Stripe subscription status (synced via webhooks). Should move from
stripeStatus: text(), // `incomplete` to `active` after the first successful payment.
stripeStatus: text().default('incomplete').notNull(),
// Whether the consumer's subscription is currently active, depending on // Whether the consumer's subscription is currently active, depending on
// `stripeStatus`. // `stripeStatus`.

Wyświetl plik

@ -193,7 +193,8 @@ Deployments are private to a developer or team until they are published, at whic
export const deploymentAdminSelectSchema = deploymentSelectSchema export const deploymentAdminSelectSchema = deploymentSelectSchema
.extend({ .extend({
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl,
_secret: z.string().nonempty()
}) })
.openapi('AdminDeployment') .openapi('AdminDeployment')

Wyświetl plik

@ -94,6 +94,7 @@ export async function upsertStripeSubscription(
) )
const updateParams: Stripe.SubscriptionUpdateParams = { const updateParams: Stripe.SubscriptionUpdateParams = {
collection_method: 'charge_automatically',
metadata: { metadata: {
userId: consumer.userId, userId: consumer.userId,
consumerId: consumer.id, consumerId: consumer.id,
@ -259,10 +260,11 @@ export async function upsertStripeSubscription(
const createParams: Stripe.SubscriptionCreateParams = { const createParams: Stripe.SubscriptionCreateParams = {
customer: stripeCustomerId, customer: stripeCustomerId,
description: `Agentic subscription to project "${project.id}"`, description: `Agentic subscription to project "${project.identifier}"`,
// TODO: coupons // TODO: coupons
// coupon: filterConsumerCoupon(ctx, consumer, deployment), // coupon: filterConsumerCoupon(ctx, consumer, deployment),
items, items,
collection_method: 'charge_automatically',
metadata: { metadata: {
userId: consumer.userId, userId: consumer.userId,
consumerId: consumer.id, consumerId: consumer.id,

Wyświetl plik

@ -1,5 +1,6 @@
import type { RawConsumerUpdate } from '@/db' import type { RawConsumerUpdate } from '@/db'
// https://docs.stripe.com/api/subscriptions/object#subscription_object-status
const stripeValidSubscriptionStatuses = new Set([ const stripeValidSubscriptionStatuses = new Set([
'active', 'active',
'trialing', 'trialing',

Wyświetl plik

@ -1,12 +1,28 @@
import { assert } from '@agentic/platform-core'
import type { Context } from './types' import type { Context } from './types'
export async function enforceRateLimit( export async function enforceRateLimit(
ctx: Context, ctx: Context,
{}: { {
id,
interval,
maxPerInterval,
method,
pathname
}: {
id: string id: string
interval: number interval: number
maxPerInterval: number maxPerInterval: number
method: string method: string
pathname: string pathname: string
} }
) {} ) {
// TODO
assert(ctx, 500, 'not implemented')
assert(id, 500, 'not implemented')
assert(interval > 0, 500, 'not implemented')
assert(maxPerInterval >= 0, 500, 'not implemented')
assert(method, 500, 'not implemented')
assert(pathname, 500, 'not implemented')
}

Wyświetl plik

@ -1,14 +1,14 @@
import type { Consumer } from '@agentic/platform-api-client'
import { assert } from '@agentic/platform-core' import { assert } from '@agentic/platform-core'
import type { Context } from './types' import type { AdminConsumer, Context } from './types'
export async function getConsumer( export async function getConsumer(
ctx: Context, ctx: Context,
token: string token: string
): Promise<Consumer> { ): Promise<AdminConsumer> {
const consumer = await ctx.client.adminGetConsumerByToken({ const consumer = await ctx.client.adminGetConsumerByToken({
token token,
populate: ['user']
}) })
assert(consumer, 404, `API token not found "${token}"`) assert(consumer, 404, `API token not found "${token}"`)

Wyświetl plik

@ -1,4 +1,4 @@
import type { AdminDeployment } from '@agentic/platform-api-client' import type { AdminDeployment } from '@agentic/platform-types'
import { assert } from '@agentic/platform-core' import { assert } from '@agentic/platform-core'
import { parseFaasIdentifier } from '@agentic/platform-validators' import { parseFaasIdentifier } from '@agentic/platform-validators'

Wyświetl plik

@ -1,13 +1,7 @@
import type { import type { PricingPlan, RateLimit } from '@agentic/platform-types'
AdminDeployment,
Consumer,
PricingPlan,
RateLimit,
Tool
} from '@agentic/platform-types'
import { assert } from '@agentic/platform-core' import { assert } from '@agentic/platform-core'
import type { Context } from './types' import type { AdminConsumer, Context, ResolvedOriginRequest } from './types'
import { getConsumer } from './get-consumer' import { getConsumer } from './get-consumer'
import { getDeployment } from './get-deployment' import { getDeployment } from './get-deployment'
import { getTool } from './get-tool' import { getTool } from './get-tool'
@ -20,11 +14,12 @@ import { updateOriginRequest } from './update-origin-request'
* Also ensures that the request is valid, enforces rate limits, and adds proxy- * Also ensures that the request is valid, enforces rate limits, and adds proxy-
* specific headers to the origin request. * specific headers to the origin request.
*/ */
export async function resolveOriginRequest(ctx: Context) { export async function resolveOriginRequest(
ctx: Context
): Promise<ResolvedOriginRequest> {
const { req } = ctx const { req } = ctx
const ip = req.headers.get('cf-connecting-ip') const ip = req.headers.get('cf-connecting-ip') || undefined
const requestUrl = new URL(req.url) const requestUrl = new URL(req.url)
const date = Date.now()
const { search, pathname } = requestUrl const { search, pathname } = requestUrl
const method = req.method.toLowerCase() const method = req.method.toLowerCase()
@ -36,18 +31,19 @@ export async function resolveOriginRequest(ctx: Context) {
deployment, deployment,
toolPath toolPath
}) })
console.log('rqeuest', {
console.log('request', {
method, method,
pathname, pathname,
search, search,
deployment: deployment.identifier, deploymentIdentifier: deployment.identifier,
toolPath, toolPath,
tool tool
}) })
let reportUsage = true
let pricingPlan: PricingPlan | undefined let pricingPlan: PricingPlan | undefined
let consumer: Consumer | undefined let consumer: AdminConsumer | undefined
let reportUsage = true
const token = (req.headers.get('authorization') || '') const token = (req.headers.get('authorization') || '')
.replace(/^Bearer /i, '') .replace(/^Bearer /i, '')
@ -177,32 +173,26 @@ export async function resolveOriginRequest(ctx: Context) {
// TODO: what do we want the API gateway's interface to be? // TODO: what do we want the API gateway's interface to be?
// - support both MCP and OpenAPI / raw? // - support both MCP and OpenAPI / raw?
const originUrl = `${deployment.originUrl}${toolPath}${search}` let originRequest: Request | undefined
console.log('originUrl', originUrl) if (
deployment.originAdapter.type === 'openapi' ||
deployment.originAdapter.type === 'raw'
) {
const originRequestUrl = `${deployment.originUrl}${toolPath}${search}`
console.log('originRequestUrl', originRequestUrl)
const originReq = new Request(originUrl, req) originRequest = new Request(originRequestUrl, req)
updateOriginRequest(originReq, { consumer, deployment, ip }) updateOriginRequest(originRequest, { consumer, deployment, ip })
}
return { return {
originReq, originRequest,
deployment: deployment.id, deployment,
project: deployment.project,
tool,
consumer, consumer,
date, tool,
ip, ip,
method, method,
plan: pricingPlan ? pricingPlan.slug : null, pricingPlanSlug: pricingPlan?.slug,
reportUsage reportUsage
} }
} }
export interface ResolvedOriginRequest {
originRequest?: Request
deployment: AdminDeployment
tool: Tool
consumer: Consumer | undefined
date: number
ip: string
method: string
}

Wyświetl plik

@ -1,4 +1,10 @@
import type { AgenticApiClient } from '@agentic/platform-api-client' import type { AgenticApiClient } from '@agentic/platform-api-client'
import type {
AdminDeployment,
Consumer,
Tool,
User
} from '@agentic/platform-types'
import type { AgenticEnv } from './env' import type { AgenticEnv } from './env'
@ -7,3 +13,18 @@ export type Context = ExecutionContext & {
env: AgenticEnv env: AgenticEnv
client: AgenticApiClient client: AgenticApiClient
} }
export interface ResolvedOriginRequest {
originRequest?: Request
deployment: AdminDeployment
consumer?: AdminConsumer
tool: Tool
method: string
reportUsage: boolean
ip?: string
pricingPlanSlug?: string
}
export type AdminConsumer = Consumer & {
user: User
}

Wyświetl plik

@ -0,0 +1,88 @@
import type { AdminDeployment } from '@agentic/platform-types'
import type { AdminConsumer } from './types'
// TODO: support custom auth providers
// const authProviders = ['github', 'google', 'spotify', 'linkedin', 'twitter']
export function updateOriginRequest(
originRequest: Request,
{
deployment,
consumer
}: {
deployment: AdminDeployment
consumer?: AdminConsumer
}
) {
// originRequest.headers.delete('authorization')
// for (const provider of authProviders) {
// const headerAccessToken = `x-${provider}-access-token`
// const headerRefreshToken = `x-${provider}-refresh-token`
// const headerAccessTokenSecret = `x-${provider}-access-token-secret`
// const headerId = `x-${provider}-id`
// const headerUsername = `x-${provider}-username`
// originRequest.headers.delete(headerAccessToken)
// originRequest.headers.delete(headerRefreshToken)
// originRequest.headers.delete(headerAccessTokenSecret)
// originRequest.headers.delete(headerId)
// originRequest.headers.delete(headerUsername)
// }
// Delete all Cloudflare headers since we want origin requests to be agnostic
// to Agentic's choice of API gateway hosting provider.
for (const headerKey of Object.keys(
Object.fromEntries(originRequest.headers.entries())
)) {
if (headerKey.startsWith('cf-')) {
originRequest.headers.delete(headerKey)
}
}
originRequest.headers.delete('x-agentic-consumer')
originRequest.headers.delete('x-agentic-user')
originRequest.headers.delete('x-agentic-plan')
originRequest.headers.delete('x-agentic-is-subscription-active')
originRequest.headers.delete('x-agentic-subscription-status')
originRequest.headers.delete('x-agentic-user-email')
originRequest.headers.delete('x-agentic-user-username')
originRequest.headers.delete('x-agentic-user-name')
originRequest.headers.delete('x-agentic-user-created-at')
originRequest.headers.delete('x-forwarded-for')
if (consumer) {
originRequest.headers.set('x-agentic-consumer', consumer.id)
originRequest.headers.set('x-agentic-user', consumer.user.id)
originRequest.headers.set(
'x-agentic-is-subscription-active',
consumer.isStripeSubscriptionActive.toString()
)
originRequest.headers.set(
'x-agentic-subscription-status',
consumer.stripeStatus
)
originRequest.headers.set('x-agentic-user-email', consumer.user.email)
originRequest.headers.set('x-agentic-user-username', consumer.user.username)
originRequest.headers.set(
'x-agentic-user-created-at',
consumer.user.createdAt
)
if (consumer.plan) {
originRequest.headers.set('x-agentic-plan', consumer.plan)
}
if (consumer.user.name) {
originRequest.headers.set('x-agentic-user-name', consumer.user.name)
}
}
// TODO: this header is causing some random upstream cloudflare errors
// https://support.cloudflare.com/hc/en-us/articles/360029779472-Troubleshooting-Cloudflare-1XXX-errors#error1000
// originRequest.headers.set('x-forwarded-for', ip)
originRequest.headers.set('x-agentic-proxy-secret', deployment._secret)
}

Wyświetl plik

@ -451,7 +451,7 @@ export interface components {
projectId: string; projectId: string;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */ /** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
deploymentId: string; deploymentId: string;
stripeStatus?: string; stripeStatus: string;
isStripeSubscriptionActive: boolean; isStripeSubscriptionActive: boolean;
}; };
/** @description Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}") */ /** @description Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}") */
@ -717,6 +717,7 @@ export interface components {
* 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;
_secret: string;
}; };
}; };
responses: { responses: {

Wyświetl plik

@ -37,6 +37,8 @@
- same for pricing plan line-items - same for pricing plan line-items
- replace `ms` package - replace `ms` package
- API gateway MCP server vs OpenAPI API gateway - API gateway MCP server vs OpenAPI API gateway
- share hono middleware and utils across apps/api and apps/gateway
- or combine these together? ehhhh
## License ## License