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')
await aclAdmin(c)
const deployment = await tryGetDeploymentByIdentifier(c, {
const { project, ...deployment } = await tryGetDeploymentByIdentifier(c, {
deploymentIdentifier,
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(
project,
404,
`Project not found for deployment "${deploymentIdentifier}"`
)
await acl(c, deployment, { label: 'Deployment' })
const hasPopulateProject = populate.includes('project')
// TODO
// TODO: switch from published to publishedAt?
// if (deployment.published) {
@ -69,7 +77,11 @@ export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
// }
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'
}),
// Stripe subscription status (synced via webhooks)
stripeStatus: text(),
// Stripe subscription status (synced via webhooks). Should move from
// `incomplete` to `active` after the first successful payment.
stripeStatus: text().default('incomplete').notNull(),
// Whether the consumer's subscription is currently active, depending on
// `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
.extend({
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl
originUrl: resolvedAgenticProjectConfigSchema.shape.originUrl,
_secret: z.string().nonempty()
})
.openapi('AdminDeployment')

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,28 @@
import { assert } from '@agentic/platform-core'
import type { Context } from './types'
export async function enforceRateLimit(
ctx: Context,
{}: {
{
id,
interval,
maxPerInterval,
method,
pathname
}: {
id: string
interval: number
maxPerInterval: number
method: 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 type { Context } from './types'
import type { AdminConsumer, Context } from './types'
export async function getConsumer(
ctx: Context,
token: string
): Promise<Consumer> {
): Promise<AdminConsumer> {
const consumer = await ctx.client.adminGetConsumerByToken({
token
token,
populate: ['user']
})
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 { parseFaasIdentifier } from '@agentic/platform-validators'

Wyświetl plik

@ -1,13 +1,7 @@
import type {
AdminDeployment,
Consumer,
PricingPlan,
RateLimit,
Tool
} from '@agentic/platform-types'
import type { PricingPlan, RateLimit } from '@agentic/platform-types'
import { assert } from '@agentic/platform-core'
import type { Context } from './types'
import type { AdminConsumer, Context, ResolvedOriginRequest } from './types'
import { getConsumer } from './get-consumer'
import { getDeployment } from './get-deployment'
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-
* specific headers to the origin request.
*/
export async function resolveOriginRequest(ctx: Context) {
export async function resolveOriginRequest(
ctx: Context
): Promise<ResolvedOriginRequest> {
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 date = Date.now()
const { search, pathname } = requestUrl
const method = req.method.toLowerCase()
@ -36,18 +31,19 @@ export async function resolveOriginRequest(ctx: Context) {
deployment,
toolPath
})
console.log('rqeuest', {
console.log('request', {
method,
pathname,
search,
deployment: deployment.identifier,
deploymentIdentifier: deployment.identifier,
toolPath,
tool
})
let reportUsage = true
let pricingPlan: PricingPlan | undefined
let consumer: Consumer | undefined
let consumer: AdminConsumer | undefined
let reportUsage = true
const token = (req.headers.get('authorization') || '')
.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?
// - support both MCP and OpenAPI / raw?
const originUrl = `${deployment.originUrl}${toolPath}${search}`
console.log('originUrl', originUrl)
let originRequest: Request | undefined
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)
updateOriginRequest(originReq, { consumer, deployment, ip })
originRequest = new Request(originRequestUrl, req)
updateOriginRequest(originRequest, { consumer, deployment, ip })
}
return {
originReq,
deployment: deployment.id,
project: deployment.project,
tool,
originRequest,
deployment,
consumer,
date,
tool,
ip,
method,
plan: pricingPlan ? pricingPlan.slug : null,
pricingPlanSlug: pricingPlan?.slug,
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 {
AdminDeployment,
Consumer,
Tool,
User
} from '@agentic/platform-types'
import type { AgenticEnv } from './env'
@ -7,3 +13,18 @@ export type Context = ExecutionContext & {
env: AgenticEnv
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;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
deploymentId: string;
stripeStatus?: string;
stripeStatus: string;
isStripeSubscriptionActive: boolean;
};
/** @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.
*/
originUrl: string;
_secret: string;
};
};
responses: {

Wyświetl plik

@ -37,6 +37,8 @@
- same for pricing plan line-items
- replace `ms` package
- 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