diff --git a/apps/api/package.json b/apps/api/package.json index b0f19a5e..1e25e155 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -54,6 +54,7 @@ "p-all": "^5.0.0", "postgres": "^3.4.5", "restore-cursor": "catalog:", + "semver": "^7.7.2", "stripe": "^18.1.0", "type-fest": "catalog:", "zod": "catalog:", @@ -61,6 +62,7 @@ }, "devDependencies": { "@types/jsonwebtoken": "^9.0.9", + "@types/semver": "^7.7.0", "drizzle-kit": "^0.31.1", "drizzle-orm": "^0.43.1" } diff --git a/apps/api/src/api-v1/consumers/update-consumer.ts b/apps/api/src/api-v1/consumers/update-consumer.ts index ffbdfa41..72183a06 100644 --- a/apps/api/src/api-v1/consumers/update-consumer.ts +++ b/apps/api/src/api-v1/consumers/update-consumer.ts @@ -16,7 +16,7 @@ import { consumerIdParamsSchema } from './schemas' const route = createRoute({ description: - "Updates a consumer's subscription to a different deployment or pricing plan.", + "Updates a consumer's subscription to a different deployment or pricing plan. Set `plan` to undefined to cancel the subscription.", tags: ['consumers'], operationId: 'updateConsumer', method: 'post', @@ -28,7 +28,7 @@ const route = createRoute({ required: true, content: { 'application/json': { - schema: schema.consumerInsertSchema + schema: schema.consumerUpdateSchema } } } diff --git a/apps/api/src/api-v1/deployments/create-deployment.ts b/apps/api/src/api-v1/deployments/create-deployment.ts new file mode 100644 index 00000000..7e5a340d --- /dev/null +++ b/apps/api/src/api-v1/deployments/create-deployment.ts @@ -0,0 +1,131 @@ +import { validators } from '@agentic/validators' +import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' +import semver from 'semver' + +import type { AuthenticatedEnv } from '@/lib/types' +import { db, eq, schema } from '@/db' +import { acl } from '@/lib/acl' +import { ensureAuthUser } from '@/lib/ensure-auth-user' +import { + openapiAuthenticatedSecuritySchemas, + openapiErrorResponse404, + openapiErrorResponse409, + openapiErrorResponse410, + openapiErrorResponses +} from '@/lib/openapi-utils' +import { assert, parseZodSchema, sha256 } from '@/lib/utils' + +const route = createRoute({ + description: 'Creates a new deployment within a project.', + tags: ['deployments'], + operationId: 'createDeployment', + method: 'post', + path: 'deployments', + security: openapiAuthenticatedSecuritySchemas, + request: { + body: { + required: true, + content: { + 'application/json': { + schema: schema.deploymentInsertSchema + } + } + } + }, + responses: { + 200: { + description: 'A deployment object', + content: { + 'application/json': { + schema: schema.deploymentSelectSchema + } + } + }, + ...openapiErrorResponses, + ...openapiErrorResponse404, + ...openapiErrorResponse409, + ...openapiErrorResponse410 + } +}) + +export function registerV1DeploymentsCreateDeployment( + app: OpenAPIHono +) { + return app.openapi(route, async (c) => { + const user = await ensureAuthUser(c) + const body = c.req.valid('json') + const teamMember = c.get('teamMember') + const { projectId } = body + + // validatePricingPlans(ctx, pricingPlans) + + // TODO: OpenAPI support + + const project = await db.query.projects.findFirst({ + where: eq(schema.projects.id, projectId), + with: { + lastPublishedDeployment: true + } + }) + assert(project, 404, `Project not found "${projectId}"`) + await acl(c, project, { label: 'Project' }) + + // TODO: investigate better short hash generation + const hash = sha256().slice(0, 8) + const deploymentId = `${project.id}@${hash}` + assert( + validators.deploymentId(deploymentId), + 400, + `Invalid deployment id "${deploymentId}"` + ) + + let { version } = body + + if (version) { + version = semver.clean(version) ?? undefined + assert( + version && semver.valid(version), + 400, + `Invalid semver version "${version}"` + ) + + const lastPublishedVersion = + project.lastPublishedDeployment?.version ?? '0.0.0' + + assert( + semver.gt(version, lastPublishedVersion), + 400, + `Semver version "${version}" must be greater than last published version "${lastPublishedVersion}"` + ) + } + + const [[deployment]] = await db.transaction(async (tx) => { + return Promise.all([ + // Create the deployment + tx + .insert(schema.deployments) + .values({ + ...body, + id: deploymentId, + hash, + userId: user.id, + teamId: teamMember?.teamId, + projectId, + version + }) + .returning(), + + // Update the project + tx + .update(schema.projects) + .set({ + lastDeploymentId: deploymentId + }) + .where(eq(schema.projects.id, projectId)) + ]) + }) + assert(deployment, 500, `Failed to create deployment "${deploymentId}"`) + + return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment)) + }) +} diff --git a/apps/api/src/api-v1/deployments/get-deployment.ts b/apps/api/src/api-v1/deployments/get-deployment.ts new file mode 100644 index 00000000..44df070e --- /dev/null +++ b/apps/api/src/api-v1/deployments/get-deployment.ts @@ -0,0 +1,58 @@ +import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' + +import type { AuthenticatedEnv } from '@/lib/types' +import { schema } from '@/db' +import { acl } from '@/lib/acl' +import { + openapiAuthenticatedSecuritySchemas, + openapiErrorResponse404, + openapiErrorResponses +} from '@/lib/openapi-utils' +import { tryGetDeployment } from '@/lib/try-get-deployment' +import { assert, parseZodSchema } from '@/lib/utils' + +import { deploymentIdParamsSchema, populateDeploymentSchema } from './schemas' + +const route = createRoute({ + description: 'Gets a deployment', + tags: ['deployments'], + operationId: 'getdeployment', + method: 'get', + path: 'deployments/{deploymentId}', + security: openapiAuthenticatedSecuritySchemas, + request: { + params: deploymentIdParamsSchema, + query: populateDeploymentSchema + }, + responses: { + 200: { + description: 'A deployment object', + content: { + 'application/json': { + schema: schema.deploymentSelectSchema + } + } + }, + ...openapiErrorResponses, + ...openapiErrorResponse404 + } +}) + +export function registerV1DeploymentsGetDeployment( + app: OpenAPIHono +) { + return app.openapi(route, async (c) => { + const { deploymentId } = c.req.valid('param') + const { populate = [] } = c.req.valid('query') + + const deployment = await tryGetDeployment(c, deploymentId, { + with: { + ...Object.fromEntries(populate.map((field) => [field, true])) + } + }) + assert(deployment, 404, `Deployment not found "${deploymentId}"`) + await acl(c, deployment, { label: 'Deployment' }) + + return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment)) + }) +} diff --git a/apps/api/src/api-v1/deployments/list-deployments.ts b/apps/api/src/api-v1/deployments/list-deployments.ts new file mode 100644 index 00000000..3b457331 --- /dev/null +++ b/apps/api/src/api-v1/deployments/list-deployments.ts @@ -0,0 +1,73 @@ +import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi' + +import type { AuthenticatedEnv } from '@/lib/types' +import { and, db, eq, schema } from '@/db' +import { + openapiAuthenticatedSecuritySchemas, + openapiErrorResponses +} from '@/lib/openapi-utils' +import { parseZodSchema } from '@/lib/utils' + +import { paginationAndPopulateAndFilterDeploymentSchema } from './schemas' + +const route = createRoute({ + description: 'Lists deployments the user or team has access to.', + tags: ['deployments'], + operationId: 'listDeployments', + method: 'get', + path: 'deployments', + security: openapiAuthenticatedSecuritySchemas, + request: { + query: paginationAndPopulateAndFilterDeploymentSchema + }, + responses: { + 200: { + description: 'A list of deployments', + content: { + 'application/json': { + schema: z.array(schema.deploymentSelectSchema) + } + } + }, + ...openapiErrorResponses + } +}) + +export function registerV1DeploymentsListDeployments( + app: OpenAPIHono +) { + return app.openapi(route, async (c) => { + const { + offset = 0, + limit = 10, + sort = 'desc', + sortBy = 'createdAt', + populate = [], + projectId + } = c.req.valid('query') + + const userId = c.get('userId') + const teamMember = c.get('teamMember') + + const deployments = await db.query.deployments.findMany({ + where: and( + teamMember + ? eq(schema.deployments.teamId, teamMember.teamId) + : eq(schema.deployments.userId, userId), + projectId ? eq(schema.deployments.projectId, projectId) : undefined + ), + with: { + ...Object.fromEntries(populate.map((field) => [field, true])) + }, + orderBy: (deployments, { asc, desc }) => [ + sort === 'desc' ? desc(deployments[sortBy]) : asc(deployments[sortBy]) + ], + offset, + limit + }) + + return c.json( + parseZodSchema(z.array(schema.deploymentSelectSchema), deployments) + ) + }) +} diff --git a/apps/api/src/api-v1/deployments/publish-deployment.ts b/apps/api/src/api-v1/deployments/publish-deployment.ts new file mode 100644 index 00000000..7bbdf977 --- /dev/null +++ b/apps/api/src/api-v1/deployments/publish-deployment.ts @@ -0,0 +1,118 @@ +import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' +import semver from 'semver' + +import type { AuthenticatedEnv } from '@/lib/types' +import { db, eq, schema } from '@/db' +import { acl } from '@/lib/acl' +import { + openapiAuthenticatedSecuritySchemas, + openapiErrorResponse404, + openapiErrorResponses +} from '@/lib/openapi-utils' +import { tryGetDeployment } from '@/lib/try-get-deployment' +import { assert, parseZodSchema } from '@/lib/utils' + +import { deploymentIdParamsSchema } from './schemas' + +const route = createRoute({ + description: 'Publishes a deployment.', + tags: ['deployments'], + operationId: 'publishDeployment', + method: 'post', + path: 'deployments/{deploymentId}/publish', + security: openapiAuthenticatedSecuritySchemas, + request: { + params: deploymentIdParamsSchema, + body: { + required: true, + content: { + 'application/json': { + schema: schema.deploymentPublishSchema + } + } + } + }, + responses: { + 200: { + description: 'A deployment object', + content: { + 'application/json': { + schema: schema.deploymentSelectSchema + } + } + }, + ...openapiErrorResponses, + ...openapiErrorResponse404 + } +}) + +export function registerV1DeploymentsPublishDeployment( + app: OpenAPIHono +) { + return app.openapi(route, async (c) => { + const { deploymentId } = c.req.valid('param') + const body = c.req.valid('json') + const version = semver.clean(body.version) + assert(version, 400, `Invalid semver version "${body.version}"`) + + // First ensure the deployment exists and the user has access to it + let deployment = await tryGetDeployment(c, deploymentId) + assert(deployment, 404, `Deployment not found "${deploymentId}"`) + await acl(c, deployment, { label: 'Deployment' }) + + const project = await db.query.projects.findFirst({ + where: eq(schema.projects.id, deployment.projectId), + with: { + lastPublishedDeployment: true + } + }) + assert(project, 404, `Project not found "${deployment.projectId}"`) + await acl(c, project, { label: 'Project' }) + + const lastPublishedVersion = + project.lastPublishedDeployment?.version || '0.0.0' + + assert( + semver.valid(version), + 400, + `Invalid semver version "${version}" for deployment "${deployment.id}"` + ) + assert( + semver.gt(version, lastPublishedVersion), + 400, + `Invalid semver version: "${version}" must be greater than current published version "${lastPublishedVersion}" for deployment "${deployment.id}"` + ) + + // TODO: enforce certain semver constraints + // - pricing changes require major version update + // - deployment shouldn't already be published? + // - any others? + + // Update the deployment and project together in a transaction + ;[[deployment]] = await db.transaction(async (tx) => { + return Promise.all([ + // Update the deployment + tx + .update(schema.deployments) + .set({ + published: true, + version + }) + .where(eq(schema.deployments.id, deploymentId)) + .returning(), + + tx + .update(schema.projects) + .set({ + lastPublishedDeploymentId: deploymentId + }) + .where(eq(schema.projects.id, project.id)) + + // TODO: add publishDeploymentLogEntry + ]) + }) + assert(deployment, 500, `Failed to update deployment "${deploymentId}"`) + + return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment)) + }) +} diff --git a/apps/api/src/api-v1/deployments/schemas.ts b/apps/api/src/api-v1/deployments/schemas.ts new file mode 100644 index 00000000..983cd60a --- /dev/null +++ b/apps/api/src/api-v1/deployments/schemas.ts @@ -0,0 +1,28 @@ +import { z } from '@hono/zod-openapi' + +import { deploymentIdSchema, paginationSchema, projectIdSchema } from '@/db' +import { deploymentRelationsSchema } from '@/db/schema' + +export const deploymentIdParamsSchema = z.object({ + deploymentId: deploymentIdSchema.openapi({ + param: { + description: 'deployment ID', + name: 'deploymentId', + in: 'path' + } + }) +}) + +export const filterDeploymentSchema = z.object({ + projectId: projectIdSchema.optional() +}) + +export const populateDeploymentSchema = z.object({ + populate: z.array(deploymentRelationsSchema).default([]).optional() +}) + +export const paginationAndPopulateAndFilterDeploymentSchema = z.object({ + ...paginationSchema.shape, + ...populateDeploymentSchema.shape, + ...filterDeploymentSchema.shape +}) diff --git a/apps/api/src/api-v1/deployments/update-deployment.ts b/apps/api/src/api-v1/deployments/update-deployment.ts new file mode 100644 index 00000000..377016a5 --- /dev/null +++ b/apps/api/src/api-v1/deployments/update-deployment.ts @@ -0,0 +1,70 @@ +import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' + +import type { AuthenticatedEnv } from '@/lib/types' +import { db, eq, schema } from '@/db' +import { acl } from '@/lib/acl' +import { + openapiAuthenticatedSecuritySchemas, + openapiErrorResponse404, + openapiErrorResponses +} from '@/lib/openapi-utils' +import { tryGetDeployment } from '@/lib/try-get-deployment' +import { assert, parseZodSchema } from '@/lib/utils' + +import { deploymentIdParamsSchema } from './schemas' + +const route = createRoute({ + description: 'Updates a deployment.', + tags: ['deployments'], + operationId: 'updateDeployment', + method: 'post', + path: 'deployments/{deploymentId}', + security: openapiAuthenticatedSecuritySchemas, + request: { + params: deploymentIdParamsSchema, + body: { + required: true, + content: { + 'application/json': { + schema: schema.deploymentUpdateSchema + } + } + } + }, + responses: { + 200: { + description: 'A deployment object', + content: { + 'application/json': { + schema: schema.deploymentSelectSchema + } + } + }, + ...openapiErrorResponses, + ...openapiErrorResponse404 + } +}) + +export function registerV1DeploymentsUpdateDeployment( + app: OpenAPIHono +) { + return app.openapi(route, async (c) => { + const { deploymentId } = c.req.valid('param') + const body = c.req.valid('json') + + // First ensure the deployment exists and the user has access to it + let deployment = await tryGetDeployment(c, deploymentId) + assert(deployment, 404, `Deployment not found "${deploymentId}"`) + await acl(c, deployment, { label: 'Deployment' }) + + // Update the deployment + ;[deployment] = await db + .update(schema.deployments) + .set(body) + .where(eq(schema.deployments.id, deploymentId)) + .returning() + assert(deployment, 500, `Failed to update deployment "${deploymentId}"`) + + return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment)) + }) +} diff --git a/apps/api/src/api-v1/index.ts b/apps/api/src/api-v1/index.ts index 65b2bec1..12ae98ef 100644 --- a/apps/api/src/api-v1/index.ts +++ b/apps/api/src/api-v1/index.ts @@ -10,6 +10,11 @@ import { registerV1ConsumersGetConsumer } from './consumers/get-consumer' import { registerV1ProjectsListConsumers } from './consumers/list-consumers' import { registerV1ConsumersRefreshConsumerToken } from './consumers/refresh-consumer-token' import { registerV1ConsumersUpdateConsumer } from './consumers/update-consumer' +import { registerV1DeploymentsCreateDeployment } from './deployments/create-deployment' +import { registerV1DeploymentsGetDeployment } from './deployments/get-deployment' +import { registerV1DeploymentsListDeployments } from './deployments/list-deployments' +import { registerV1DeploymentsPublishDeployment } from './deployments/publish-deployment' +import { registerV1DeploymentsUpdateDeployment } from './deployments/update-deployment' import { registerHealthCheck } from './health-check' import { registerV1ProjectsCreateProject } from './projects/create-project' import { registerV1ProjectsGetProject } from './projects/get-project' @@ -76,17 +81,6 @@ registerV1ProjectsListProjects(privateRouter) registerV1ProjectsGetProject(privateRouter) registerV1ProjectsUpdateProject(privateRouter) -// TODO -// pub.get('/projects/alias/:alias(.+)', require('./projects').readByAlias) -// pri.get('/projects/provider/:project(.+)', require('./provider').read) -// pri.put('/projects/provider/:project(.+)', require('./provider').update) -// pri.put('/projects/connect/:project(.+)', require('./projects').connect) -// pub.get( -// '/projects/:project(.+)', -// middleware.authenticate({ passthrough: true }), -// require('./projects').read -// ) - // Consumers registerV1ConsumersGetConsumer(privateRouter) registerV1ConsumersCreateConsumer(privateRouter) @@ -94,6 +88,13 @@ registerV1ConsumersUpdateConsumer(privateRouter) registerV1ConsumersRefreshConsumerToken(privateRouter) registerV1ProjectsListConsumers(privateRouter) +// Deployments +registerV1DeploymentsGetDeployment(privateRouter) +registerV1DeploymentsCreateDeployment(privateRouter) +registerV1DeploymentsUpdateDeployment(privateRouter) +registerV1DeploymentsListDeployments(privateRouter) +registerV1DeploymentsPublishDeployment(privateRouter) + // Webhook event handlers registerV1StripeWebhook(publicRouter) @@ -135,3 +136,9 @@ export type ApiRoutes = | ReturnType | ReturnType | ReturnType + // Deployments + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType diff --git a/apps/api/src/api-v1/projects/create-project.ts b/apps/api/src/api-v1/projects/create-project.ts index fdc7d5ca..feba746c 100644 --- a/apps/api/src/api-v1/projects/create-project.ts +++ b/apps/api/src/api-v1/projects/create-project.ts @@ -2,7 +2,6 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' import type { AuthenticatedEnv } from '@/lib/types' import { db, schema } from '@/db' -import { aclTeamMember } from '@/lib/acl-team-member' import { createProviderToken } from '@/lib/auth/create-provider-token' import { ensureAuthUser } from '@/lib/ensure-auth-user' import { @@ -48,9 +47,9 @@ export function registerV1ProjectsCreateProject( const body = c.req.valid('json') const user = await ensureAuthUser(c) - if (body.teamId) { - await aclTeamMember(c, { teamId: body.teamId }) - } + // if (body.teamId) { + // await aclTeamMember(c, { teamId: body.teamId }) + // } const teamMember = c.get('teamMember') const namespace = teamMember ? teamMember.teamSlug : user.username diff --git a/apps/api/src/db/schema/deployment.ts b/apps/api/src/db/schema/deployment.ts index efa857a7..313bcec9 100644 --- a/apps/api/src/db/schema/deployment.ts +++ b/apps/api/src/db/schema/deployment.ts @@ -62,7 +62,7 @@ export const deployments = pgTable( // TODO: third-party auth provider config? // Backend API URL - _url: text().notNull(), + originUrl: text().notNull(), // Array pricingPlans: jsonb().$type().notNull() @@ -96,6 +96,13 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({ }) })) +export type DeploymentRelationFields = keyof ReturnType< + (typeof deploymentsRelations)['config'] +> + +export const deploymentRelationsSchema: z.ZodType = + z.enum(['user', 'team', 'project']) + // TODO: virtual hasFreeTier // TODO: virtual url // TODO: virtual openApiUrl @@ -106,28 +113,6 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({ export const deploymentSelectSchema = createSelectSchema(deployments, { // build: z.object({}), // env: z.object({}), - - pricingPlans: pricingPlanListSchema - // coupons: z.array(couponSchema) -}) - .omit({ - _url: true - }) - .extend({ - user: z - .lazy(() => userSelectSchema) - .optional() - .openapi('User', { type: 'object' }), - - team: z - .lazy(() => teamSelectSchema) - .optional() - .openapi('Team', { type: 'object' }) - }) - .strip() - .openapi('Deployment') - -export const deploymentInsertSchema = createInsertSchema(deployments, { id: (schema) => schema.refine((id) => validators.deploymentId(id), { message: 'Invalid deployment id' @@ -138,28 +123,63 @@ export const deploymentInsertSchema = createInsertSchema(deployments, { message: 'Invalid deployment hash' }), + pricingPlans: pricingPlanListSchema + // coupons: z.array(couponSchema) +}) + .omit({ + originUrl: true + }) + .extend({ + user: z + .lazy(() => userSelectSchema) + .optional() + .openapi('User', { type: 'object' }), + + team: z + .lazy(() => teamSelectSchema) + .optional() + .openapi('Team', { type: 'object' }), + + // TODO: Circular references make this schema less than ideal + project: z.object({}).optional().openapi('Project', { type: 'object' }) + }) + .strip() + .openapi('Deployment') + +export const deploymentInsertSchema = createInsertSchema(deployments, { projectId: (schema) => schema.refine((id) => validators.projectId(id), { message: 'Invalid project id' }), - _url: (schema) => schema.url(), + originUrl: (schema) => schema.url(), pricingPlans: pricingPlanListSchema // TODOp // coupons: z.array(couponSchema).optional() }) - .omit({ id: true, createdAt: true, updatedAt: true }) + .omit({ + id: true, + createdAt: true, + updatedAt: true, + hash: true, + userId: true, + teamId: true + }) .strict() export const deploymentUpdateSchema = createUpdateSchema(deployments) .pick({ enabled: true, - published: true, - version: true, description: true }) .strict() -// TODO: add admin select schema which includes all fields? +export const deploymentPublishSchema = createUpdateSchema(deployments, { + version: z.string().nonempty() +}) + .pick({ + version: true + }) + .strict() diff --git a/apps/api/src/db/schema/project.ts b/apps/api/src/db/schema/project.ts index bb6e1826..d8e2f397 100644 --- a/apps/api/src/db/schema/project.ts +++ b/apps/api/src/db/schema/project.ts @@ -18,8 +18,7 @@ import { type StripePriceIdMap, stripePriceIdMapSchema, type StripeProductIdMap, - stripeProductIdMapSchema, - type Webhook + stripeProductIdMapSchema } from './schemas' import { teams, teamSelectSchema } from './team' import { users, userSelectSchema } from './user' @@ -77,13 +76,11 @@ export const projects = pgTable( // All deployments share the same underlying proxy secret _secret: text().notNull(), - // Auth token used to access the saasify API on behalf of this project + // Auth token used to access the platform API on behalf of this project _providerToken: text().notNull(), // TODO: Full-text search - _text: text().default('').notNull(), - - _webhooks: jsonb().$type().default([]).notNull(), + // _text: text().default('').notNull(), // Stripe coupons associated with this project, mapping from unique coupon // object hash to stripe coupon id. @@ -184,8 +181,7 @@ export const projectSelectSchema = createSelectSchema(projects, { .omit({ _secret: true, _providerToken: true, - _text: true, - _webhooks: true, + // _text: true, _stripeProductIdMap: true, _stripePriceIdMap: true, _stripeMeterIdMap: true, @@ -227,8 +223,7 @@ export const projectInsertSchema = createInsertSchema(projects, { }) }) .pick({ - name: true, - teamId: true + name: true }) .strict() diff --git a/apps/api/src/lib/billing/upsert-consumer.ts b/apps/api/src/lib/billing/upsert-consumer.ts index 9aadd57f..afc9844a 100644 --- a/apps/api/src/lib/billing/upsert-consumer.ts +++ b/apps/api/src/lib/billing/upsert-consumer.ts @@ -18,7 +18,7 @@ export async function upsertConsumer( deploymentId, consumerId }: { - plan: string + plan?: string deploymentId?: string consumerId?: string } diff --git a/apps/api/src/lib/middleware/team.ts b/apps/api/src/lib/middleware/team.ts index d1692e5c..62a3e0d4 100644 --- a/apps/api/src/lib/middleware/team.ts +++ b/apps/api/src/lib/middleware/team.ts @@ -19,7 +19,7 @@ export const team = createMiddleware( eq(schema.teamMembers.userId, user.id) ) }) - assert(teamMember, 401, 'Unauthorized') + assert(teamMember, 403, 'Unauthorized') await aclTeamMember(ctx, { teamMember }) diff --git a/apps/api/src/lib/try-get-deployment.ts b/apps/api/src/lib/try-get-deployment.ts index 88cfb9e0..d2952eaa 100644 --- a/apps/api/src/lib/try-get-deployment.ts +++ b/apps/api/src/lib/try-get-deployment.ts @@ -8,8 +8,6 @@ import { assert } from './utils' /** * Attempts to find the Deployment matching the given identifier. - * - * If the Deployment is not found, throw an HttpError. */ export async function tryGetDeployment( ctx: AuthenticatedContext, @@ -21,7 +19,7 @@ export async function tryGetDeployment( project?: true } } = {} -): Promise { +): Promise { const user = await ensureAuthUser(ctx) const teamMember = ctx.get('teamMember') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faea2126..190cfe45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,6 +179,9 @@ importers: restore-cursor: specifier: 'catalog:' version: 5.1.0 + semver: + specifier: ^7.7.2 + version: 7.7.2 stripe: specifier: ^18.1.0 version: 18.1.0(@types/node@22.15.18) @@ -195,6 +198,9 @@ importers: '@types/jsonwebtoken': specifier: ^9.0.9 version: 9.0.9 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.0 drizzle-kit: specifier: ^0.31.1 version: 0.31.1 @@ -1133,6 +1139,9 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -4278,6 +4287,8 @@ snapshots: pg-protocol: 1.10.0 pg-types: 2.2.0 + '@types/semver@7.7.0': {} + '@types/shimmer@1.2.0': {} '@types/tedious@4.0.14':