feat: add private/public to projects and expose public projects for marketplace

pull/715/head
Travis Fischer 2025-06-17 09:21:22 +07:00
rodzic 844453584a
commit ee5783e780
49 zmienionych plików z 809 dodań i 185 usunięć

Wyświetl plik

@ -4,7 +4,7 @@ import { assert } from '@agentic/platform-core'
import { authStorage } from './utils'
export function registerV1AuthGitHubOAuthCallback(
export function registerV1GitHubOAuthCallback(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.get('auth/github/callback', async (c) => {

Wyświetl plik

@ -51,7 +51,7 @@ const route = createRoute({
}
})
export function registerV1AuthGitHubOAuthExchange(
export function registerV1GitHubOAuthExchange(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -35,7 +35,7 @@ const route = createRoute({
}
})
export function registerV1AuthGitHubOAuthInitFlow(
export function registerV1GitHubOAuthInitFlow(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -49,9 +49,7 @@ const route = createRoute({
}
})
export function registerV1AuthSignInWithPassword(
app: OpenAPIHono<DefaultHonoEnv>
) {
export function registerV1SignInWithPassword(app: OpenAPIHono<DefaultHonoEnv>) {
return app.openapi(route, trySignIn)
}

Wyświetl plik

@ -52,9 +52,7 @@ const route = createRoute({
}
})
export function registerV1AuthSignUpWithPassword(
app: OpenAPIHono<DefaultHonoEnv>
) {
export function registerV1SignUpWithPassword(app: OpenAPIHono<DefaultHonoEnv>) {
return app.openapi(route, async (c) => {
try {
// try signing in to see if the user already exists

Wyświetl plik

@ -42,7 +42,7 @@ const route = createRoute({
}
})
export function registerV1AdminConsumersActivateConsumer(
export function registerV1AdminActivateConsumer(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -38,7 +38,7 @@ const route = createRoute({
}
})
export function registerV1AdminConsumersGetConsumerByToken(
export function registerV1AdminGetConsumerByToken(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -45,7 +45,7 @@ const route = createRoute({
}
})
export function registerV1ConsumersCreateConsumer(
export function registerV1CreateConsumer(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -37,9 +37,7 @@ const route = createRoute({
}
})
export function registerV1ConsumersGetConsumer(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1GetConsumer(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { consumerId } = c.req.valid('param')
const { populate = [] } = c.req.valid('query')

Wyświetl plik

@ -36,7 +36,7 @@ const route = createRoute({
}
})
export function registerV1ConsumersListConsumers(
export function registerV1ListConsumers(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -38,7 +38,7 @@ const route = createRoute({
}
})
export function registerV1ConsumersListForProject(
export function registerV1ListConsumersForProject(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -37,7 +37,7 @@ const route = createRoute({
}
})
export function registerV1ConsumersRefreshConsumerToken(
export function registerV1RefreshConsumerToken(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -49,7 +49,7 @@ const route = createRoute({
}
})
export function registerV1ConsumersUpdateConsumer(
export function registerV1UpdateConsumer(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -39,7 +39,7 @@ const route = createRoute({
}
})
export function registerV1AdminDeploymentsGetDeploymentByIdentifier(
export function registerV1AdminGetDeploymentByIdentifier(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -2,7 +2,7 @@ import { resolveAgenticProjectConfig } from '@agentic/platform'
import { assert, parseZodSchema, sha256 } from '@agentic/platform-core'
import {
isValidDeploymentIdentifier,
isValidProjectIdentifier
parseProjectIdentifier
} from '@agentic/platform-validators'
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
@ -54,7 +54,7 @@ const route = createRoute({
}
})
export function registerV1DeploymentsCreateDeployment(
export function registerV1CreateDeployment(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {
@ -64,13 +64,10 @@ export function registerV1DeploymentsCreateDeployment(
const teamMember = c.get('teamMember')
const logger = c.get('logger')
const namespace = teamMember ? teamMember.teamSlug : user.username
const projectIdentifier = `@${namespace}/${body.name}`
assert(
isValidProjectIdentifier(projectIdentifier),
400,
`Invalid project identifier "${projectIdentifier}"`
)
const inputNamespace = teamMember ? teamMember.teamSlug : user.username
const inputProjectIdentifier = `@${inputNamespace}/${body.name}`
const { projectIdentifier, projectNamespace, projectName } =
parseProjectIdentifier(inputProjectIdentifier)
let project = await db.query.projects.findFirst({
where: eq(schema.projects.identifier, projectIdentifier),
@ -88,8 +85,9 @@ export function registerV1DeploymentsCreateDeployment(
await db
.insert(schema.projects)
.values({
name: body.name,
identifier: projectIdentifier,
namespace: projectNamespace,
name: projectName,
userId: user.id,
teamId: teamMember?.teamId,
_secret: await sha256()

Wyświetl plik

@ -37,7 +37,7 @@ const route = createRoute({
}
})
export function registerV1DeploymentsGetDeploymentByIdentifier(
export function registerV1GetDeploymentByIdentifier(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -38,7 +38,7 @@ const route = createRoute({
}
})
export function registerV1DeploymentsGetDeployment(
export function registerV1GetDeployment(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -37,7 +37,7 @@ const route = createRoute({
}
})
export function registerV1DeploymentsListDeployments(
export function registerV1ListDeployments(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -46,7 +46,7 @@ const route = createRoute({
}
})
export function registerV1DeploymentsPublishDeployment(
export function registerV1PublishDeployment(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -45,7 +45,7 @@ const route = createRoute({
}
})
export function registerV1DeploymentsUpdateDeployment(
export function registerV1UpdateDeployment(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -6,42 +6,45 @@ import type { AuthenticatedHonoEnv } from '@/lib/types'
import * as middleware from '@/lib/middleware'
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
import { registerV1AuthGitHubOAuthCallback } from './auth/github-callback'
import { registerV1AuthGitHubOAuthExchange } from './auth/github-exchange'
import { registerV1AuthGitHubOAuthInitFlow } from './auth/github-init'
import { registerV1AuthSignInWithPassword } from './auth/sign-in-with-password'
import { registerV1AuthSignUpWithPassword } from './auth/sign-up-with-password'
import { registerV1AdminConsumersActivateConsumer } from './consumers/admin-activate-consumer'
import { registerV1AdminConsumersGetConsumerByToken } from './consumers/admin-get-consumer-by-token'
import { registerV1ConsumersCreateConsumer } from './consumers/create-consumer'
import { registerV1ConsumersGetConsumer } from './consumers/get-consumer'
import { registerV1ConsumersListConsumers } from './consumers/list-consumers'
import { registerV1ConsumersListForProject } from './consumers/list-project-consumers'
import { registerV1ConsumersRefreshConsumerToken } from './consumers/refresh-consumer-token'
import { registerV1ConsumersUpdateConsumer } from './consumers/update-consumer'
import { registerV1AdminDeploymentsGetDeploymentByIdentifier } from './deployments/admin-get-deployment-by-identifier'
import { registerV1DeploymentsCreateDeployment } from './deployments/create-deployment'
import { registerV1DeploymentsGetDeployment } from './deployments/get-deployment'
import { registerV1DeploymentsGetDeploymentByIdentifier } from './deployments/get-deployment-by-identifier'
import { registerV1DeploymentsListDeployments } from './deployments/list-deployments'
import { registerV1DeploymentsPublishDeployment } from './deployments/publish-deployment'
import { registerV1DeploymentsUpdateDeployment } from './deployments/update-deployment'
import { registerV1GitHubOAuthCallback } from './auth/github-callback'
import { registerV1GitHubOAuthExchange } from './auth/github-exchange'
import { registerV1GitHubOAuthInitFlow } from './auth/github-init'
import { registerV1SignInWithPassword } from './auth/sign-in-with-password'
import { registerV1SignUpWithPassword } from './auth/sign-up-with-password'
import { registerV1AdminActivateConsumer } from './consumers/admin-activate-consumer'
import { registerV1AdminGetConsumerByToken } from './consumers/admin-get-consumer-by-token'
import { registerV1CreateConsumer } from './consumers/create-consumer'
import { registerV1GetConsumer } from './consumers/get-consumer'
import { registerV1ListConsumers } from './consumers/list-consumers'
import { registerV1ListConsumersForProject } from './consumers/list-project-consumers'
import { registerV1RefreshConsumerToken } from './consumers/refresh-consumer-token'
import { registerV1UpdateConsumer } from './consumers/update-consumer'
import { registerV1AdminGetDeploymentByIdentifier } from './deployments/admin-get-deployment-by-identifier'
import { registerV1CreateDeployment } from './deployments/create-deployment'
import { registerV1GetDeployment } from './deployments/get-deployment'
import { registerV1GetDeploymentByIdentifier } from './deployments/get-deployment-by-identifier'
import { registerV1ListDeployments } from './deployments/list-deployments'
import { registerV1PublishDeployment } from './deployments/publish-deployment'
import { registerV1UpdateDeployment } from './deployments/update-deployment'
import { registerHealthCheck } from './health-check'
import { registerV1ProjectsCreateProject } from './projects/create-project'
import { registerV1ProjectsGetProject } from './projects/get-project'
import { registerV1ProjectsGetProjectByIdentifier } from './projects/get-project-by-identifier'
import { registerV1ProjectsListProjects } from './projects/list-projects'
import { registerV1ProjectsUpdateProject } from './projects/update-project'
import { registerV1TeamsCreateTeam } from './teams/create-team'
import { registerV1TeamsDeleteTeam } from './teams/delete-team'
import { registerV1TeamsGetTeam } from './teams/get-team'
import { registerV1TeamsListTeams } from './teams/list-teams'
import { registerV1TeamsMembersCreateTeamMember } from './teams/members/create-team-member'
import { registerV1TeamsMembersDeleteTeamMember } from './teams/members/delete-team-member'
import { registerV1TeamsMembersUpdateTeamMember } from './teams/members/update-team-member'
import { registerV1TeamsUpdateTeam } from './teams/update-team'
import { registerV1UsersGetUser } from './users/get-user'
import { registerV1UsersUpdateUser } from './users/update-user'
import { registerV1CreateProject } from './projects/create-project'
import { registerV1GetProject } from './projects/get-project'
import { registerV1GetProjectByIdentifier } from './projects/get-project-by-identifier'
import { registerV1GetPublicProject } from './projects/get-public-project'
import { registerV1GetPublicProjectByIdentifier } from './projects/get-public-project-by-identifier'
import { registerV1ListProjects } from './projects/list-projects'
import { registerV1ListPublicProjects } from './projects/list-public-projects'
import { registerV1UpdateProject } from './projects/update-project'
import { registerV1CreateTeam } from './teams/create-team'
import { registerV1DeleteTeam } from './teams/delete-team'
import { registerV1GetTeam } from './teams/get-team'
import { registerV1ListTeams } from './teams/list-teams'
import { registerV1CreateTeamMember } from './teams/members/create-team-member'
import { registerV1DeleteTeamMember } from './teams/members/delete-team-member'
import { registerV1UpdateTeamMember } from './teams/members/update-team-member'
import { registerV1UpdateTeam } from './teams/update-team'
import { registerV1GetUser } from './users/get-user'
import { registerV1UpdateUser } from './users/update-user'
import { registerV1StripeWebhook } from './webhooks/stripe-webhook'
// Note that the order of some of these routes is important because of
@ -79,55 +82,60 @@ const privateRouter = new OpenAPIHono<AuthenticatedHonoEnv>()
registerHealthCheck(publicRouter)
// Auth
registerV1AuthSignInWithPassword(publicRouter)
registerV1AuthSignUpWithPassword(publicRouter)
registerV1AuthGitHubOAuthExchange(publicRouter)
registerV1AuthGitHubOAuthInitFlow(publicRouter)
registerV1AuthGitHubOAuthCallback(publicRouter)
registerV1SignInWithPassword(publicRouter)
registerV1SignUpWithPassword(publicRouter)
registerV1GitHubOAuthExchange(publicRouter)
registerV1GitHubOAuthInitFlow(publicRouter)
registerV1GitHubOAuthCallback(publicRouter)
// Users
registerV1UsersGetUser(privateRouter)
registerV1UsersUpdateUser(privateRouter)
registerV1GetUser(privateRouter)
registerV1UpdateUser(privateRouter)
// Teams
registerV1TeamsCreateTeam(privateRouter)
registerV1TeamsListTeams(privateRouter)
registerV1TeamsGetTeam(privateRouter)
registerV1TeamsDeleteTeam(privateRouter)
registerV1TeamsUpdateTeam(privateRouter)
registerV1CreateTeam(privateRouter)
registerV1ListTeams(privateRouter)
registerV1GetTeam(privateRouter)
registerV1DeleteTeam(privateRouter)
registerV1UpdateTeam(privateRouter)
// Team members
registerV1TeamsMembersCreateTeamMember(privateRouter)
registerV1TeamsMembersUpdateTeamMember(privateRouter)
registerV1TeamsMembersDeleteTeamMember(privateRouter)
registerV1CreateTeamMember(privateRouter)
registerV1UpdateTeamMember(privateRouter)
registerV1DeleteTeamMember(privateRouter)
// Projects
registerV1ProjectsCreateProject(privateRouter)
registerV1ProjectsListProjects(privateRouter)
registerV1ProjectsGetProjectByIdentifier(privateRouter) // must be before `registerV1ProjectsGetProject`
registerV1ProjectsGetProject(privateRouter)
registerV1ProjectsUpdateProject(privateRouter)
// Public projects
registerV1ListPublicProjects(publicRouter)
registerV1GetPublicProjectByIdentifier(publicRouter) // must be before `registerV1GetPublicProject`
registerV1GetPublicProject(publicRouter)
// Private projects
registerV1CreateProject(privateRouter)
registerV1ListProjects(privateRouter)
registerV1GetProjectByIdentifier(privateRouter) // must be before `registerV1GetProject`
registerV1GetProject(privateRouter)
registerV1UpdateProject(privateRouter)
// Consumers
registerV1ConsumersGetConsumer(privateRouter)
registerV1ConsumersCreateConsumer(privateRouter)
registerV1ConsumersUpdateConsumer(privateRouter)
registerV1ConsumersRefreshConsumerToken(privateRouter)
registerV1ConsumersListConsumers(privateRouter)
registerV1ConsumersListForProject(privateRouter)
registerV1GetConsumer(privateRouter)
registerV1CreateConsumer(privateRouter)
registerV1UpdateConsumer(privateRouter)
registerV1RefreshConsumerToken(privateRouter)
registerV1ListConsumers(privateRouter)
registerV1ListConsumersForProject(privateRouter)
// Deployments
registerV1DeploymentsGetDeploymentByIdentifier(privateRouter) // must be before `registerV1DeploymentsGetDeployment`
registerV1DeploymentsGetDeployment(privateRouter)
registerV1DeploymentsCreateDeployment(privateRouter)
registerV1DeploymentsUpdateDeployment(privateRouter)
registerV1DeploymentsListDeployments(privateRouter)
registerV1DeploymentsPublishDeployment(privateRouter)
registerV1GetDeploymentByIdentifier(privateRouter) // must be before `registerV1GetDeployment`
registerV1GetDeployment(privateRouter)
registerV1CreateDeployment(privateRouter)
registerV1UpdateDeployment(privateRouter)
registerV1ListDeployments(privateRouter)
registerV1PublishDeployment(privateRouter)
// Internal admin routes
registerV1AdminConsumersGetConsumerByToken(privateRouter)
registerV1AdminConsumersActivateConsumer(privateRouter)
registerV1AdminDeploymentsGetDeploymentByIdentifier(privateRouter)
registerV1AdminGetConsumerByToken(privateRouter)
registerV1AdminActivateConsumer(privateRouter)
registerV1AdminGetDeploymentByIdentifier(privateRouter)
// Webhook event handlers
registerV1StripeWebhook(publicRouter)

Wyświetl plik

@ -1,5 +1,5 @@
import { assert, parseZodSchema, sha256 } from '@agentic/platform-core'
import { isValidProjectIdentifier } from '@agentic/platform-validators'
import { parseProjectIdentifier } from '@agentic/platform-validators'
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
import type { AuthenticatedHonoEnv } from '@/lib/types'
@ -40,7 +40,7 @@ const route = createRoute({
}
})
export function registerV1ProjectsCreateProject(
export function registerV1CreateProject(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {
@ -54,8 +54,9 @@ export function registerV1ProjectsCreateProject(
const teamMember = c.get('teamMember')
const namespace = teamMember ? teamMember.teamSlug : user.username
const identifier = `@${namespace}/${body.name}`
const parsedProjectIdentifier = parseProjectIdentifier(identifier)
assert(
isValidProjectIdentifier(identifier),
parsedProjectIdentifier,
400,
`Invalid project identifier "${identifier}"`
)
@ -64,7 +65,9 @@ export function registerV1ProjectsCreateProject(
.insert(schema.projects)
.values({
...body,
identifier,
identifier: parsedProjectIdentifier.projectIdentifier,
namespace: parsedProjectIdentifier.projectNamespace,
name: parsedProjectIdentifier.projectName,
teamId: teamMember?.teamId,
userId: user.id,
_secret: await sha256()

Wyświetl plik

@ -37,7 +37,7 @@ const route = createRoute({
}
})
export function registerV1ProjectsGetProjectByIdentifier(
export function registerV1GetProjectByIdentifier(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -37,9 +37,7 @@ const route = createRoute({
}
})
export function registerV1ProjectsGetProject(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1GetProject(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { projectId } = c.req.valid('param')
const { populate = [] } = c.req.valid('query')

Wyświetl plik

@ -0,0 +1,60 @@
import type { DefaultHonoEnv } from '@agentic/platform-hono'
import { assert, parseZodSchema } from '@agentic/platform-core'
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
import { db, eq, schema } from '@/db'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponse404,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { projectIdentifierAndPopulateSchema } from './schemas'
const route = createRoute({
description:
'Gets a public project by its public identifier (eg, "@username/project-name").',
tags: ['projects'],
operationId: 'getPublicProjectByIdentifier',
method: 'get',
path: 'projects/public/by-identifier',
security: openapiAuthenticatedSecuritySchemas,
request: {
query: projectIdentifierAndPopulateSchema
},
responses: {
200: {
description: 'A project',
content: {
'application/json': {
schema: schema.projectSelectSchema
}
}
},
...openapiErrorResponses,
...openapiErrorResponse404
}
})
export function registerV1GetPublicProjectByIdentifier(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.openapi(route, async (c) => {
const { projectIdentifier, populate = [] } = c.req.valid('query')
const project = await db.query.projects.findFirst({
where: eq(schema.projects.identifier, projectIdentifier),
with: {
lastPublishedDeployment: true,
...Object.fromEntries(populate.map((field) => [field, true]))
}
})
assert(
project && project.private && project.lastPublishedDeploymentId,
404,
`Public project not found "${projectIdentifier}"`
)
return c.json(parseZodSchema(schema.projectSelectSchema, project))
})
}

Wyświetl plik

@ -0,0 +1,59 @@
import type { DefaultHonoEnv } from '@agentic/platform-hono'
import { assert, parseZodSchema } from '@agentic/platform-core'
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
import { db, eq, schema } from '@/db'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponse404,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { populateProjectSchema, projectIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Gets a public project by ID.',
tags: ['projects'],
operationId: 'getPublicProject',
method: 'get',
path: 'projects/public/{projectId}',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: projectIdParamsSchema,
query: populateProjectSchema
},
responses: {
200: {
description: 'A project',
content: {
'application/json': {
schema: schema.projectSelectSchema
}
}
},
...openapiErrorResponses,
...openapiErrorResponse404
}
})
export function registerV1GetPublicProject(app: OpenAPIHono<DefaultHonoEnv>) {
return app.openapi(route, async (c) => {
const { projectId } = c.req.valid('param')
const { populate = [] } = c.req.valid('query')
const project = await db.query.projects.findFirst({
where: eq(schema.projects.id, projectId),
with: {
lastPublishedDeployment: true,
...Object.fromEntries(populate.map((field) => [field, true]))
}
})
assert(
project && project.private && project.lastPublishedDeploymentId,
404,
`Public project not found "${projectId}"`
)
return c.json(parseZodSchema(schema.projectSelectSchema, project))
})
}

Wyświetl plik

@ -12,7 +12,7 @@ import {
import { paginationAndPopulateProjectSchema } from './schemas'
const route = createRoute({
description: 'Lists projects the authenticated user has access to.',
description: 'Lists projects owned by the authenticated user or team.',
tags: ['projects'],
operationId: 'listProjects',
method: 'get',
@ -34,9 +34,7 @@ const route = createRoute({
}
})
export function registerV1ProjectsListProjects(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1ListProjects(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const {
offset = 0,

Wyświetl plik

@ -0,0 +1,66 @@
import type { DefaultHonoEnv } from '@agentic/platform-hono'
import { parseZodSchema } from '@agentic/platform-core'
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
import { and, db, eq, isNotNull, schema } from '@/db'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { paginationAndPopulateProjectSchema } from './schemas'
const route = createRoute({
description:
'Lists projects that have been published publicly to the marketplace.',
tags: ['projects'],
operationId: 'listPublicProjects',
method: 'get',
path: 'projects/public',
security: openapiAuthenticatedSecuritySchemas,
request: {
query: paginationAndPopulateProjectSchema
},
responses: {
200: {
description: 'A list of projects',
content: {
'application/json': {
schema: z.array(schema.projectSelectSchema)
}
}
},
...openapiErrorResponses
}
})
export function registerV1ListPublicProjects(app: OpenAPIHono<DefaultHonoEnv>) {
return app.openapi(route, async (c) => {
const {
offset = 0,
limit = 10,
sort = 'desc',
sortBy = 'createdAt',
populate = []
} = c.req.valid('query')
const projects = await db.query.projects.findMany({
// List projects that are not private and have at least one published deployment
where: and(
eq(schema.projects.private, false),
isNotNull(schema.projects.lastPublishedDeploymentId)
),
with: {
lastPublishedDeployment: true,
...Object.fromEntries(populate.map((field) => [field, true]))
},
orderBy: (projects, { asc, desc }) => [
sort === 'desc' ? desc(projects[sortBy]) : asc(projects[sortBy])
],
offset,
limit
})
return c.json(parseZodSchema(z.array(schema.projectSelectSchema), projects))
})
}

Wyświetl plik

@ -1,3 +1,4 @@
// import { isValidNamespace } from '@agentic/platform-validators'
import { z } from '@hono/zod-openapi'
import {
@ -17,6 +18,21 @@ export const projectIdParamsSchema = z.object({
})
})
// export const namespaceParamsSchema = z.object({
// namespace: z
// .string()
// .refine((namespace) => isValidNamespace(namespace), {
// message: 'Invalid namespace'
// })
// .openapi({
// param: {
// description: 'Namespace',
// name: 'namespace',
// in: 'path'
// }
// })
// })
export const projectIdentifierQuerySchema = z.object({
projectIdentifier: projectIdentifierSchema
})

Wyświetl plik

@ -44,7 +44,7 @@ const route = createRoute({
}
})
export function registerV1ProjectsUpdateProject(
export function registerV1UpdateProject(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -40,9 +40,7 @@ const route = createRoute({
}
})
export function registerV1TeamsCreateTeam(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1CreateTeam(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const user = await ensureAuthUser(c)
const body = c.req.valid('json')

Wyświetl plik

@ -36,9 +36,7 @@ const route = createRoute({
}
})
export function registerV1TeamsDeleteTeam(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1DeleteTeam(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { teamId } = c.req.valid('param')
await aclTeamAdmin(c, { teamId })

Wyświetl plik

@ -36,7 +36,7 @@ const route = createRoute({
}
})
export function registerV1TeamsGetTeam(app: OpenAPIHono<AuthenticatedHonoEnv>) {
export function registerV1GetTeam(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { teamId } = c.req.valid('param')
await aclTeamMember(c, { teamId })

Wyświetl plik

@ -31,9 +31,7 @@ const route = createRoute({
}
})
export function registerV1TeamsListTeams(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1ListTeams(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const {
offset = 0,

Wyświetl plik

@ -46,7 +46,7 @@ const route = createRoute({
}
})
export function registerV1TeamsMembersCreateTeamMember(
export function registerV1CreateTeamMember(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -37,7 +37,7 @@ const route = createRoute({
}
})
export function registerV1TeamsMembersDeleteTeamMember(
export function registerV1DeleteTeamMember(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -45,7 +45,7 @@ const route = createRoute({
}
})
export function registerV1TeamsMembersUpdateTeamMember(
export function registerV1UpdateTeamMember(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -44,9 +44,7 @@ const route = createRoute({
}
})
export function registerV1TeamsUpdateTeam(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1UpdateTeam(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { teamId } = c.req.valid('param')
const body = c.req.valid('json')

Wyświetl plik

@ -36,7 +36,7 @@ const route = createRoute({
}
})
export function registerV1UsersGetUser(app: OpenAPIHono<AuthenticatedHonoEnv>) {
export function registerV1GetUser(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { userId } = c.req.valid('param')
await acl(c, { userId }, { label: 'User' })

Wyświetl plik

@ -44,9 +44,7 @@ const route = createRoute({
}
})
export function registerV1UsersUpdateUser(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
export function registerV1UpdateUser(app: OpenAPIHono<AuthenticatedHonoEnv>) {
return app.openapi(route, async (c) => {
const { userId } = c.req.valid('param')
await acl(c, { userId }, { label: 'User' })

Wyświetl plik

@ -10,6 +10,7 @@ import {
import { isValidProjectName } from '@agentic/platform-validators'
import { relations } from '@fisch0920/drizzle-orm'
import {
boolean,
index,
integer,
jsonb,
@ -62,8 +63,16 @@ export const projects = pgTable(
...timestamps,
identifier: projectIdentifier().unique().notNull(),
namespace: text().notNull(),
name: text().notNull(),
alias: text(),
// Defaulting to `true` for now to hide all projects from the marketplace
// by default. Will need to manually set to `true` to allow projects to be
// visible on the marketplace.
private: boolean().default(true).notNull(),
// TODO: allow for multiple aliases like vercel
// alias: text(),
userId: userId()
.notNull()
@ -144,9 +153,14 @@ export const projects = pgTable(
},
(table) => [
uniqueIndex('project_identifier_idx').on(table.identifier),
index('project_namespace_idx').on(table.namespace),
index('project_userId_idx').on(table.userId),
index('project_teamId_idx').on(table.teamId),
index('project_alias_idx').on(table.alias),
// index('project_alias_idx').on(table.alias),
index('project_private_idx').on(table.private),
index('project_lastPublishedDeploymentId_idx').on(
table.lastPublishedDeploymentId
),
index('project_createdAt_idx').on(table.createdAt),
index('project_updatedAt_idx').on(table.updatedAt),
index('project_deletedAt_idx').on(table.deletedAt)
@ -250,8 +264,8 @@ export const projectInsertSchema = createInsertSchema(projects, {
export const projectUpdateSchema = createUpdateSchema(projects)
.pick({
name: true,
alias: true
name: true
// alias: true
})
.strict()

Wyświetl plik

@ -1,7 +1,7 @@
import { AgenticApiClient } from '@agentic/platform-api-client'
import { env } from '../src/env'
import { deployFixtures } from '../src/deploy-fixtures'
import { env } from '../src/env'
export const client = new AgenticApiClient({
apiBaseUrl: env.AGENTIC_API_BASE_URL

Wyświetl plik

@ -0,0 +1,116 @@
'use client'
import { useInfiniteQuery } from '@tanstack/react-query'
import Link from 'next/link'
import useInfiniteScroll from 'react-infinite-scroll-hook'
import { useAgentic } from '@/components/agentic-provider'
import { LoadingIndicator } from '@/components/loading-indicator'
import { toastError } from '@/lib/notifications'
export function MarketplaceIndex() {
const ctx = useAgentic()
const limit = 10
const {
data,
isLoading,
isError,
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) =>
ctx!.api
.listPublicProjects({
populate: ['lastPublishedDeployment'],
offset: pageParam,
limit
})
.then(async (projects) => {
return {
projects,
offset: pageParam,
limit,
nextOffset:
projects.length >= limit ? pageParam + projects.length : undefined
}
})
.catch((err: any) => {
void toastError('Failed to fetch projects')
throw err
}),
getNextPageParam: (lastGroup) => lastGroup?.nextOffset,
enabled: !!ctx,
initialPageParam: 0
})
const [sentryRef] = useInfiniteScroll({
loading: isLoading || isFetchingNextPage,
hasNextPage,
onLoadMore: fetchNextPage,
disabled: !ctx || isError,
rootMargin: '0px 0px 200px 0px'
})
const projects = data ? data.pages.flatMap((p) => p.projects) : []
return (
<>
<section>
<h1
className='text-center text-balance leading-snug md:leading-none
text-4xl font-extrabold'
>
Dashboard
</h1>
{!ctx || isLoading ? (
<LoadingIndicator />
) : (
<div className='mt-8'>
<h2 className='text-xl font-semibold mb-4'>Your Projects</h2>
{isError ? (
<p>Error fetching projects</p>
) : !projects.length ? (
<p>
No projects found. Create your first project to get started!
</p>
) : (
<div className='grid gap-4'>
{projects.map((project) => (
<Link
key={project.id}
className='p-4 border rounded-lg hover:border-gray-400 transition-colors'
href={`/app/projects/${project.identifier}`}
>
<h3 className='font-medium'>{project.name}</h3>
<p className='text-sm text-gray-500'>
{project.identifier}
</p>
{project.lastPublishedDeployment && (
<p className='text-sm text-gray-500 mt-1'>
Last published:{' '}
{project.lastPublishedDeployment.version ||
project.lastPublishedDeployment.hash}
</p>
)}
</Link>
))}
{hasNextPage && (
<div ref={sentryRef} className=''>
{isLoading || (isFetchingNextPage && <LoadingIndicator />)}
</div>
)}
</div>
)}
</div>
)}
</section>
</>
)
}

Wyświetl plik

@ -0,0 +1,5 @@
import { MarketplaceIndex } from './marketplace-index'
export default function MarketplaceIndexPage() {
return <MarketplaceIndex />
}

Wyświetl plik

@ -0,0 +1,58 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useAgentic } from '@/components/agentic-provider'
import { LoadingIndicator } from '@/components/loading-indicator'
import { toastError } from '@/lib/notifications'
export function MarketplaceProjectIndex({
projectIdentifier
}: {
projectIdentifier: string
}) {
const ctx = useAgentic()
const {
data: project,
isLoading,
isError
} = useQuery({
queryKey: ['project', projectIdentifier],
queryFn: () =>
ctx!.api
.getPublicProjectByIdentifier({
projectIdentifier,
populate: ['lastPublishedDeployment']
})
.catch((err: any) => {
void toastError(`Failed to fetch project "${projectIdentifier}"`)
throw err
}),
enabled: !!ctx
})
return (
<section>
{!ctx || isLoading ? (
<LoadingIndicator />
) : isError ? (
<p>Error fetching project</p>
) : !project ? (
<p>Project "{projectIdentifier}" not found</p>
) : (
<>
<h1
className='text-center text-balance leading-snug md:leading-none
text-4xl font-extrabold'
>
Project {project.name}
</h1>
<div className='mt-8'>
<pre className='max-w-lg'>{JSON.stringify(project, null, 2)}</pre>
</div>
</>
)}
</section>
)
}

Wyświetl plik

@ -0,0 +1,34 @@
import { parseProjectIdentifier } from '@agentic/platform-validators'
import { notFound } from 'next/navigation'
import { toastError } from '@/lib/notifications'
import { MarketplaceProjectIndex } from './marketplace-project-index'
export default async function MarketplaceProjectIndexPage({
params
}: {
params: Promise<{
namespace: string
'project-name': string
}>
}) {
const { namespace: rawNamespace, 'project-name': rawProjectName } =
await params
try {
const namespace = decodeURIComponent(rawNamespace)
const projectName = decodeURIComponent(rawProjectName)
const { projectIdentifier } = parseProjectIdentifier(
`${namespace}/${projectName}`,
{ strict: true }
)
return <MarketplaceProjectIndex projectIdentifier={projectIdentifier} />
} catch (err: any) {
void toastError(err, { label: 'Invalid project identifier' })
return notFound()
}
}

Wyświetl plik

@ -230,9 +230,10 @@ export class AgenticApiClient {
/** Gets the currently authenticated user. */
async getMe(): Promise<User> {
// const user = await this.verifyAuthAndRefreshIfNecessary()
assert(this._authSession)
const userId = this._authSession?.user.id
assert(userId, 'This method requires authentication.')
return this.ky.get(`v1/users/${this._authSession.user.id}`).json()
return this.ky.get(`v1/users/${userId}`).json()
}
/** Gets a user by ID. */
@ -335,7 +336,65 @@ export class AgenticApiClient {
.json()
}
/** Lists projects the authenticated user has access to. */
/** Lists projects that have been published publicly to the marketplace. */
async listPublicProjects<
TPopulate extends NonNullable<
OperationParameters<'listPublicProjects'>['populate']
>[number]
>(
searchParams: OperationParameters<'listPublicProjects'> & {
populate?: TPopulate[]
} = {}
): Promise<Array<PopulateProject<TPopulate>>> {
return this.ky
.get('v1/projects', { searchParams: sanitizeSearchParams(searchParams) })
.json()
}
/**
* Gets a public project by ID. The project must be publicly available on
* the marketplace.
*/
async getPublicProject<
TPopulate extends NonNullable<
OperationParameters<'getPublicProject'>['populate']
>[number]
>({
projectId,
...searchParams
}: OperationParameters<'getPublicProject'> & {
populate?: TPopulate[]
}): Promise<PopulateProject<TPopulate>> {
return this.ky
.get(`v1/projects/public/${projectId}`, {
searchParams: sanitizeSearchParams(searchParams)
})
.json()
}
/**
* Gets a public project by its identifier. The project must be publicly
* available on the marketplace.
*/
async getPublicProjectByIdentifier<
TPopulate extends NonNullable<
OperationParameters<'getPublicProjectByIdentifier'>['populate']
>[number]
>(
searchParams: OperationParameters<'getPublicProjectByIdentifier'> & {
populate?: TPopulate[]
}
): Promise<PopulateProject<TPopulate>> {
return this.ky
.get('v1/projects/public/by-identifier', {
searchParams: sanitizeSearchParams(searchParams)
})
.json()
}
/**
* Lists projects the authenticated user has access to.
*/
async listProjects<
TPopulate extends NonNullable<
OperationParameters<'listProjects'>['populate']
@ -346,7 +405,9 @@ export class AgenticApiClient {
} = {}
): Promise<Array<PopulateProject<TPopulate>>> {
return this.ky
.get('v1/projects', { searchParams: sanitizeSearchParams(searchParams) })
.get(`v1/projects`, {
searchParams: sanitizeSearchParams(searchParams)
})
.json()
}
@ -358,7 +419,9 @@ export class AgenticApiClient {
return this.ky.post('v1/projects', { json: project, searchParams }).json()
}
/** Gets a project by ID. */
/**
* Gets a project by ID. Authenticated user must have access to the project.
*/
async getProject<
TPopulate extends NonNullable<
OperationParameters<'getProject'>['populate']
@ -376,7 +439,10 @@ export class AgenticApiClient {
.json()
}
/** Gets a project by its public identifier. */
/**
* Gets a project by its identifier. Authenticated user must have access to
* the project.
*/
async getProjectByIdentifier<
TPopulate extends NonNullable<
OperationParameters<'getProjectByIdentifier'>['populate']
@ -393,7 +459,9 @@ export class AgenticApiClient {
.json()
}
/** Updates a project. */
/**
* Updates a project. Authenticated user must have access to the project.
*/
async updateProject(
project: OperationBody<'updateProject'>,
{ projectId, ...searchParams }: OperationParameters<'updateProject'>

Wyświetl plik

@ -89,6 +89,57 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/projects/public": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Lists projects that have been published publicly to the marketplace. */
get: operations["listPublicProjects"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/projects/public/by-identifier": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Gets a public project by its public identifier (eg, "@username/project-name"). */
get: operations["getPublicProjectByIdentifier"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/projects/public/{projectId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Gets a public project by ID. */
get: operations["getPublicProject"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/users/{userId}": {
parameters: {
query?: never;
@ -186,7 +237,7 @@ export interface paths {
path?: never;
cookie?: never;
};
/** @description Lists projects the authenticated user has access to. */
/** @description Lists projects owned by the authenticated user or team. */
get: operations["listProjects"];
put?: never;
/** @description Creates a new project. */
@ -445,6 +496,39 @@ export interface components {
token: string;
user: components["schemas"]["User"];
};
/** @description Public project identifier (e.g. "@namespace/project-name") */
ProjectIdentifier: string;
/** @description The frequency at which a subscription is billed. */
PricingInterval: "day" | "week" | "month" | "year";
/** @description 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). */
Project: {
/** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */
id: string;
createdAt: string;
updatedAt: string;
deletedAt?: string;
identifier: components["schemas"]["ProjectIdentifier"];
namespace: string;
name: string;
private: boolean;
/** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */
userId: string;
/** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */
teamId?: string;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
lastPublishedDeploymentId?: string;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
lastDeploymentId?: string;
lastPublishedDeploymentVersion?: string;
applicationFeePercent: number;
defaultPricingInterval: components["schemas"]["PricingInterval"];
/** @enum {string} */
pricingCurrency: "usd";
};
Team: {
id: string;
createdAt: string;
@ -466,38 +550,6 @@ export interface components {
confirmed: boolean;
confirmedAt?: string;
};
/** @description Public project identifier (e.g. "@namespace/project-name") */
ProjectIdentifier: string;
/** @description The frequency at which a subscription is billed. */
PricingInterval: "day" | "week" | "month" | "year";
/** @description 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). */
Project: {
/** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */
id: string;
createdAt: string;
updatedAt: string;
deletedAt?: string;
identifier: components["schemas"]["ProjectIdentifier"];
name: string;
alias?: string;
/** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */
userId: string;
/** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */
teamId?: string;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
lastPublishedDeploymentId?: string;
/** @description Deployment id (e.g. "depl_tz4a98xxat96iws9zmbrgj3a") */
lastDeploymentId?: string;
lastPublishedDeploymentVersion?: string;
applicationFeePercent: number;
defaultPricingInterval: components["schemas"]["PricingInterval"];
/** @enum {string} */
pricingCurrency: "usd";
};
/** @description A Consumer represents a user who has subscribed to a Project and is used
* to track usage and billing.
*
@ -1065,6 +1117,92 @@ export interface operations {
404: components["responses"]["404"];
};
};
listPublicProjects: {
parameters: {
query?: {
offset?: number | null;
limit?: number;
sort?: "asc" | "desc";
sortBy?: "createdAt" | "updatedAt";
populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description A list of projects */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Project"][];
};
};
400: components["responses"]["400"];
401: components["responses"]["401"];
403: components["responses"]["403"];
};
};
getPublicProjectByIdentifier: {
parameters: {
query: {
populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[];
/** @description Public project identifier (e.g. "@namespace/project-name") */
projectIdentifier: components["schemas"]["ProjectIdentifier"];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description A project */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Project"];
};
};
400: components["responses"]["400"];
401: components["responses"]["401"];
403: components["responses"]["403"];
404: components["responses"]["404"];
};
};
getPublicProject: {
parameters: {
query?: {
populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[];
};
header?: never;
path: {
/** @description Project ID */
projectId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description A project */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Project"];
};
};
400: components["responses"]["400"];
401: components["responses"]["401"];
403: components["responses"]["403"];
404: components["responses"]["404"];
};
};
getUser: {
parameters: {
query?: never;
@ -1505,7 +1643,6 @@ export interface operations {
content: {
"application/json": {
name?: string;
alias?: string;
};
};
};

Wyświetl plik

@ -30,6 +30,8 @@
- hosted docs
- merge with current agentic repo
- publish packages to npm
- api
- deploy to prod
- database
- consider using [neon serverless driver](https://orm.drizzle.team/docs/connect-neon) for production
- can this also be used locally?