import { oc } from '@orpc/contract' import { z } from 'zod' import type { AssetDependencies } from '../schemas.js' import type { AssetFileEntry, AssetInstallMetadata, AssetSearchResult, AssetVersion, PaginatedList, User, } from '../contract.js' import { MAX_UPLOAD_ZIP_SIZE_BYTES, agentRunStatusSchema, agentStartSchema, agentStatusSchema, assetCollectionReadSchema, assetQueriesSchema, generateAssetSchema, generateJobStatusSchema, generateResponseSchema, generateStatusInputSchema, updateProfileSchema, uploadZipSchema, } from '../schemas.js' /** * The v1 manifest of an asset version — the one linkable, cacheable answer to "what is this * version": its metadata plus every stored file with a fetch-ready content-plane URL, the preview * URL, and the zip URL. All URLs are minted by the server and MUST be treated as opaque by * clients; that opacity is what lets the content plane evolve without client releases. */ export interface AssetVersionManifest { name: string type: string version: string ownerId: string description: string | null approved: boolean access: 'public' | 'private' npmDependencies: Record assetDependencies: AssetDependencies skillDependencies: Record files: AssetFileEntry[] previewUrl: string | null zipUrl: string } /** One entry of the refs read: the version's full manifest plus the asset's version history * (oldest first), so range selection and multi-version hashing need no further calls. */ export interface AssetRefEntry extends AssetVersionManifest { versions: string[] } export interface AssetRefsResult { assets: (AssetRefEntry | null)[] } export interface AssetQueryResult { type: string query: string | null startAfter: number total: number assets: AssetSearchResult[] } /** * The v1 contract: the same procedure tree as the legacy RPC surface (so call sites migrate by * swapping only the client), with HTTP routes attached — served as plain REST under `/api/v1`. * The route annotations here ARE the control-plane URL contract. */ export const contract = { asset: { // The one collection read, discriminated by params: `?refs=name[@version],…` // returns exactly those assets (full entries, order-preserved, null per // miss); everything else is a search. Reads are plural by default. search: oc .route({ method: 'GET', path: '/assets' }) .input(assetCollectionReadSchema) .output(z.custom | AssetRefsResult>()), query: oc .route({ method: 'POST', path: '/assets' }) .input(assetQueriesSchema) .output(z.custom<{ results: AssetQueryResult[] }>()), uploadZip: oc .route({ method: 'PUT', path: '/assets/{name}/{version}' }) .input( uploadZipSchema .extend({ zip: z.instanceof(File) }) .refine((input) => input.zip.size < MAX_UPLOAD_ZIP_SIZE_BYTES, { message: 'Upload zip must be smaller than 1 GB', path: ['zip'], }), ) .output(z.custom()), installMetadata: oc .route({ method: 'GET', path: '/types' }) .output(z.custom>()), generate: oc .route({ method: 'POST', path: '/generations' }) .input(generateAssetSchema) .output(generateResponseSchema), generateStatus: oc .route({ method: 'GET', path: '/generations/{jobId}' }) .input(generateStatusInputSchema) .output(generateJobStatusSchema), agent: { start: oc .route({ method: 'POST', path: '/agent-runs' }) .input(agentStartSchema) .output(z.object({ runId: z.string() })), status: oc .route({ method: 'GET', path: '/agent-runs/{runId}' }) .input(agentStatusSchema) .output(agentRunStatusSchema), }, }, user: { getProfile: oc.route({ method: 'GET', path: '/me' }).output(z.custom()), updateProfile: oc .route({ method: 'PATCH', path: '/me' }) .input(updateProfileSchema) .output(z.custom()), getAuthToken: oc .route({ method: 'GET', path: '/me/auth-token' }) .output(z.custom<{ prefix: string; createdAt: Date } | null>()), createAuthToken: oc .route({ method: 'POST', path: '/me/auth-token' }) .output(z.object({ token: z.string(), prefix: z.string() })), }, } export type V1Contract = typeof contract