import { type Context, Hono } from 'hono' import * as z from 'zod/mini' import * as ApiKeys from '../../../ApiKeys.js' import type * as App from '../../../App.js' import * as Auth from '../../../internal/Auth.js' import * as Db from '../../../db/Db.js' import * as OpenApi from '../../../internal/OpenApi.js' import * as Projects from '../../../db/tables/projects.js' import * as Response from '../../../internal/Response.js' import * as Schema from '../../../internal/Schema.js' /** Environments every project exposes; sandbox maps to testnet, production to mainnet. */ const environments = ['production', 'sandbox'] as const /** Zod schemas owned by the projects resource. */ export namespace schema { const name = z .string() .check( z.minLength(1), z.maxLength(100), z.describe('Human-readable name.'), z.meta({ examples: ['Checkout'] }), ) /** One project: an app or integration within an organization. */ export const Project = OpenApi.component( Schema.describe( z.object({ createdAt: z.iso .datetime() .check( z.describe('When the project was created (ISO 8601).'), z.meta({ examples: ['2026-01-01T00:00:00.000Z'] }), ), environments: z.array(z.enum(environments)).check( z.describe('Environments available to this project: sandbox (testnet) and production (mainnet).'), // prettier-ignore z.meta({ examples: [[...environments]] }), ), id: z .string() .check( z.describe('Opaque project id (`prj_…`).'), z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), name: z .string() .check(z.describe('Human-readable project name.'), z.meta({ examples: ['Checkout'] })), orgId: z .string() .check( z.describe('Owning organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), updatedAt: z.iso .datetime() .check( z.describe('When the project was last updated (ISO 8601).'), z.meta({ examples: ['2026-01-01T00:00:00.000Z'] }), ), }), 'A project: an app or integration within an organization.', ), 'Project', ) /** Path parameters addressing an organization's project collection. */ export const Params = z .object({ orgId: z .string() .check( z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe("Path parameters for an organization's projects.")) /** Path parameters addressing one project. */ export const ProjectParams = z .object({ orgId: z .string() .check( z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), projectId: z .string() .check( z.describe('The project id (`prj_…`).'), z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe('Path parameters for one project.')) /** Request body creating a project. */ export const CreateProjectRequest = OpenApi.component( z.object({ name }).check(z.describe('Fields for creating a project.')), 'CreateProjectRequest', ) /** Request body updating a project. */ export const UpdateProjectRequest = OpenApi.component( z.object({ name }).check(z.describe('Fields for updating a project.')), 'UpdateProjectRequest', ) /** Schemas for the deleteProject operation. */ export namespace deleteProject { /** Confirmation that the project was deleted. */ export const Response = OpenApi.component( z .object({ id: z .string() .check( z.describe('ID of the project that was deleted.'), z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe('Confirmation that the project was deleted.')), 'DeleteProjectResponse', ) } /** Schemas for the listProjects operation. */ export namespace listProjects { /** Non-paginated list of an organization's projects. */ export const Response = OpenApi.component( Schema.describe( z.object({ data: z.array(Project).check(z.describe('The projects, newest first.')), }), "A non-paginated list of an organization's projects.", ), 'ProjectList', ) } } /** * Mounts projects under their owning organization. Access follows the scoped session, API key, or super admin; inaccessible projects return `404`. */ export function projects() { return new Hono() .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects', Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.Params, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ operationId: 'listProjects', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path parameters.', }, 404: { codes: ['organization_not_found'], description: 'No accessible organization was found for this id.', }, }, success: { description: "The organization's projects, newest first.", schema: schema.listProjects.Response, }, }), summary: 'List projects', tags: ['Projects'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) const db = Db.get(c.get('db')) try { const records = await Projects.listByOrg(db, Auth.org(c).id) return c.json( Response.validated(schema.listProjects.Response, { data: records.map(serializeProject), }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .post( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects', Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.Params, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.validate('json', schema.CreateProjectRequest, { code: 'body_invalid', message: 'Check the request body and try again.', }), OpenApi.describeRoute({ operationId: 'createProject', responses: OpenApi.responses({ errors: { 400: { codes: ['body_invalid', 'param_invalid'], description: 'Malformed API key, invalid path parameters, or invalid request body.', }, 404: { codes: ['organization_not_found'], description: 'No accessible organization was found for this id.', }, }, success: { description: 'The created project.', schema: schema.Project }, }), summary: 'Create project', tags: ['Projects'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'body_invalid', message: 'Check the request body and try again.', }) const body = c.req.valid('json') const db = Db.get(c.get('db')) try { const record = await Projects.create(db, { name: body.name, orgId: Auth.org(c).id }) return c.json(Response.validated(schema.Project, serializeProject(record)), 200) } catch (cause) { return Response.upstream(c, cause) } }, ) .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }), Auth.ensureOrg(), Auth.ensureProject(), OpenApi.validate('param', schema.ProjectParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ operationId: 'getProject', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path parameters.', }, 404: { codes: ['organization_not_found', 'project_not_found'], description: 'No accessible project was found for this id.', }, }, success: { description: 'One project.', schema: schema.Project }, }), summary: 'Get project', tags: ['Projects'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) return c.json(Response.validated(schema.Project, serializeProject(Auth.project(c))), 200) }, ) .patch( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }), Auth.ensureOrg({ role: 'admin' }), Auth.ensureProject(), OpenApi.validate('param', schema.ProjectParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.validate('json', schema.UpdateProjectRequest, { code: 'body_invalid', message: 'Check the request body and try again.', }), OpenApi.describeRoute({ operationId: 'updateProject', responses: OpenApi.responses({ errors: { 400: { codes: ['body_invalid', 'param_invalid'], description: 'Malformed API key, invalid path parameters, or invalid request body.', }, 404: { codes: ['organization_not_found', 'project_not_found'], description: 'No accessible project was found for this id.', }, }, success: { description: 'The updated project.', schema: schema.Project }, }), summary: 'Update project', tags: ['Projects'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'body_invalid', message: 'Check the request body and try again.', }) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) const body = c.req.valid('json') const db = Db.get(c.get('db')) try { const updated = await Projects.update(db, Auth.project(c).id, { name: body.name }) if (!updated) return projectNotFound(c) return c.json(Response.validated(schema.Project, serializeProject(updated)), 200) } catch (cause) { return Response.upstream(c, cause) } }, ) .delete( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }), Auth.ensureOrg({ role: 'admin' }), Auth.ensureProject(), OpenApi.validate('param', schema.ProjectParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ operationId: 'deleteProject', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path parameters.', }, 404: { codes: ['organization_not_found', 'project_not_found'], description: 'No accessible project was found for this id.', }, }, success: { description: 'Confirmation that the project was deleted.', schema: schema.deleteProject.Response, }, }), summary: 'Delete project', tags: ['Projects'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) const db = Db.get(c.get('db')) try { const kv = c.get('kv') if (kv) for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, { projectId: Auth.project(c).id, scopeCatalog: c.get('scopeCatalog'), })) await ApiKeys.revoke(kv.store, record.id) const deleted = await Projects.deleteProject(db, Auth.project(c).id) if (!deleted) return projectNotFound(c) return c.json( Response.validated(schema.deleteProject.Response, { id: Auth.project(c).id }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) } function serializeProject(record: Projects.Record) { return { createdAt: record.createdAt, environments: [...environments], id: record.id, name: record.name, orgId: record.orgId, updatedAt: record.updatedAt, } } function projectNotFound(c: Context) { return Response.error(c, { code: 'project_not_found', message: 'Project not found', status: 404, }) }