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, assetNameSchema, assetQueriesSchema, generateAssetSchema, generateJobStatusSchema, generateResponseSchema, generateStatusInputSchema, semverSchema, 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 downloadCount: number 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[] } const assetDownloadReceiptSchema = z.object({ receiptId: z.string().uuid(), assets: z .array( z.object({ name: assetNameSchema, version: semverSchema, }), ) .min(1) .max(1000), }) export const installReceiptSchema = z.object({ assetCount: z.number().int().nonnegative(), assetTypes: z.array(z.string().min(1).max(64)).max(64), fileCount: z.number().int().nonnegative(), npmDependencyCount: z.number().int().nonnegative(), redirectedFileCount: z.number().int().nonnegative(), skillDependencyCount: z.number().int().nonnegative(), warningCount: z.number().int().nonnegative(), /** Added after v1 shipped; omitted receipts remain valid for older published clients. */ downloadReceipt: assetDownloadReceiptSchema.optional(), }) /** * 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), }, }, installation: { complete: oc .route({ method: 'POST', path: '/installs' }) .input(installReceiptSchema) .output(z.object({ accepted: z.literal(true) })), }, 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