diff --git a/.github/workflows/builds.yaml b/.github/workflows/builds.yaml index 3ef54007..aa6148ad 100644 --- a/.github/workflows/builds.yaml +++ b/.github/workflows/builds.yaml @@ -48,5 +48,8 @@ jobs: - name: Mock Portfolio Template Library run: node scripts/mock-template-library.mjs + - name: Build Shared Packages + run: npm run build -w @veriworkly/profile-core --if-present + - name: Build Application run: npm run build -w ${{ matrix.workspace }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 53796d44..df039d7b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -35,6 +35,9 @@ jobs: - name: Prisma Generate run: npm exec -w @veriworkly/server -- prisma generate --schema=prisma/schema.prisma + - name: Build Shared Packages + run: npm run build -w @veriworkly/profile-core --if-present + - name: Run Backend Tests run: npm run test -w @veriworkly/server --if-present @@ -73,6 +76,9 @@ jobs: - name: Install Dependencies run: npm ci --legacy-peer-deps + - name: Build Shared Packages + run: npm run build -w @veriworkly/profile-core --if-present + - name: Run Studio Tests run: npm run test:contracts -w @veriworkly/studio --if-present @@ -92,6 +98,9 @@ jobs: - name: Install Dependencies run: npm ci --legacy-peer-deps + - name: Build Shared Packages + run: npm run build -w @veriworkly/profile-core --if-present + - name: Mock Portfolio Template Library run: node scripts/mock-template-library.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0491c63..8b985ff1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ First off, thank you for considering contributing to **VeriWorkly**! We are buil > [!IMPORTANT] > **Detailed guides, developer workflows, and coding standards** live in our official documentation: > -> - ๐Ÿ“– **[Full Contributing Guidelines](https://docs.veriworkly.com/docs/contributing/index)** +> - ๐Ÿ“– **[Full Contributing Guidelines](https://docs.veriworkly.com/docs/contributing)** > - ๐Ÿ› ๏ธ **[Detailed Local Setup Guide](https://docs.veriworkly.com/docs/getting-started/local-setup)** --- @@ -36,7 +36,7 @@ Before getting started, please **star our repository** to show your support for 2. Clone your fork locally: ```bash git clone https://github.com/YOUR_USERNAME/veriworkly.git - cd veriworkly-resume + cd veriworkly ``` 3. Set up the upstream remote: ```bash @@ -61,10 +61,10 @@ Before getting started, please **star our repository** to show your support for ### 5. Local Setup & Verification ๐Ÿ› ๏ธ - Install dependencies: `npm install` -- Copy environment variables: `cp .env.example .env` and `cp apps/server/.env.example apps/server/.env` +- Copy environment variables: copy all `.env.example` files to `.env` in root and across `apps/` (see `ENV_SETUP.md` or `QUICK_START.md`). - Run the app locally to test your changes: - **Frontend-only (Site/Templates at port 3000)**: `npm run dev` - - **Full-stack (All apps/databases)**: `npm run dev:all` _(Requires running `npm run db:push -w @veriworkly/server`)_ + - **Full-stack (All apps/databases)**: `npm run dev:all` _(Requires running `npm run db:push && npm run db:generate`)_ --- diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 41a3517d..f0af86de 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -1,28 +1,69 @@ # Environment Variables Configuration -VeriWorkly requires specific environment variables to function correctly across the frontend and backend. +VeriWorkly uses a multi-app monorepo structure where each application and the root workspace define specific `.env.example` templates. -## ๐Ÿ”‘ Critical Variables +--- -### Backend (`apps/server/.env`) +## ๐Ÿ”‘ Workspace Environment Configurations -- `DATABASE_URL`: PostgreSQL connection string. -- `AUTH_SECRET`: Random secure string for authentication. -- `AUTH_SMTP_HOST`, `AUTH_SMTP_PORT`, `AUTH_SMTP_USER`, `AUTH_SMTP_PASS`: For sending OTP emails. -- `REDIS_URL`: (Optional) For rate limiting and API caching. +### 1. Root Monorepo (`.env`) -### Frontend Studio (`apps/studio/.env`) +- `NODE_ENV`: Runtime environment (`development`, `production`, `test`). +- `PORT`: Port bindings for multi-workspace coordination. -- `NEXT_PUBLIC_BACKEND_URL`: URL of the backend server (default: `http://localhost:8080/api/v1`). -- `NEXT_PUBLIC_APP_URL`: URL of the studio app (default: `http://localhost:3001`). -- `AUTH_SECRET`: Same secret as backend. +### 2. Backend Server (`apps/server/.env`) -### Marketing Site (`apps/site/.env`) +- `DATABASE_URL`: PostgreSQL connection string (supports Neon / Supabase / local PostgreSQL). +- `AUTH_SECRET`: Random 32+ character secure secret for Better-Auth encryption. +- `REDIS_URL`: **Required.** Backs Better-Auth secondary session storage, API rate limiting, ATS and profile import quotas, view count buffers, and distributed cron job locks. +- `AUTH_SMTP_HOST`, `AUTH_SMTP_PORT`, `AUTH_SMTP_USER`, `AUTH_SMTP_PASS`, `AUTH_SMTP_FROM`: SMTP credentials for passwordless email OTP authentication. +- `ADMIN_EMAIL`: Email address of the administrative superuser. +- `DODO_PAYMENTS_API_KEY`, `DODO_PAYMENTS_WEBHOOK_KEY`: Dodo Payments API and webhook credentials. +- `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET_NAME`, `R2_PUBLIC_URL`: Cloudflare R2 bucket credentials for portfolio media asset uploads. +- `AI_GATEWAY_AUTH_TOKEN`: Private API token for frontier LLM routing. -- `NEXT_PUBLIC_BACKEND_URL`: URL of the backend server. -- `NEXT_PUBLIC_APP_URL`: URL of the marketing site (default: `http://localhost:3000`). +### 3. Builder Studio (`apps/studio/.env`) -## ๐Ÿ“š Detailed Variable Reference +- `SITE_URL`: Marketing site base URL (default: `http://localhost:3000`). +- `NEXT_PUBLIC_BACKEND_URL`: Public API endpoint for browser calls (default: `http://localhost:8080/api/v1`). +- `BACKEND_INTERNAL_URL`: Server-side API endpoint for Next.js SSR requests (default: `http://localhost:8080/api/v1`). +- `NEXT_PUBLIC_PORTFOLIO_URL`: Portfolio builder workspace URL (default: `http://localhost:3004`). +- `ADMIN_EMAIL`: Matches server admin email for admin UI surfaces. +- `AUTH_SECRET`: Matches server auth secret. -For a full list of all available configuration options and their defaults, visit: +### 4. Marketing Site (`apps/site/.env`) + +- `NEXT_PUBLIC_SITE_URL` / `SITE_URL`: Canonical public site origin (default: `http://localhost:3000`). +- `NEXT_PUBLIC_BACKEND_URL`: Public API endpoint (default: `http://localhost:8080/api/v1`). +- `BACKEND_INTERNAL_URL`: Server-side API endpoint (default: `http://localhost:8080/api/v1`). +- `ADMIN_EMAIL`: Admin email for checkout/pricing preview bypass. +- `AFFILIATE_PROGRAM_ENABLED`, `AMBASSADOR_PROGRAM_ENABLED`: Boot-time feature flags for growth programs. + +### 5. Portfolio Builder (`apps/portfolio/.env`) + +- `SITE_URL`: Public origin for portfolio app (default: `http://localhost:3004`). +- `NEXT_PUBLIC_PORTFOLIO_URL`: Studio app URL (default: `http://localhost:3001`). +- `NEXT_PUBLIC_BACKEND_URL`: Public API endpoint (default: `http://localhost:8080/api/v1`). +- `BACKEND_INTERNAL_URL`: Server-side API endpoint (default: `http://localhost:8080/api/v1`). +- `ADMIN_EMAIL`: Admin email for checkout/publish staging gate bypass. +- `PORTFOLIO_REVALIDATE_SECRET`: Secret token for Next.js on-demand ISR revalidation. + +### 6. Documentation Platform (`apps/docs-platform/.env`) + +- `SITE_URL`: Docs origin URL (default: `http://localhost:3002`). +- `NEXT_PUBLIC_BACKEND_URL`: Public API endpoint (default: `http://localhost:8080/api/v1`). +- `BACKEND_INTERNAL_URL`: Server-side API endpoint (default: `http://localhost:8080/api/v1`). +- `ALLOWED_ORIGINS`: Allowed origins for API proxy. + +### 7. Blog Platform (`apps/blog-platform/.env`) + +- `SITE_URL`: Blog origin URL (default: `http://localhost:3003`). +- `NEXT_PUBLIC_BACKEND_URL`: Public API endpoint (default: `http://localhost:8080/api/v1`). +- `BACKEND_INTERNAL_URL`: Server-side API endpoint (default: `http://localhost:8080/api/v1`). + +--- + +## ๐Ÿ“š Complete Operations Reference + +For exhaustive configuration parameters, defaults, and deployment guides, visit: [Environment Variables Guide - VeriWorkly Docs](https://docs.veriworkly.com/docs/operations/environment-variables) diff --git a/PRODUCT.md b/PRODUCT.md index 58f3482c..88a14cec 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -30,4 +30,4 @@ Avoid surveillance-heavy resume SaaS, generic chatbot interfaces, opaque AI gene ## Accessibility & Inclusion -Target WCAG 2.1 AA. All editor actions require keyboard access, visible focus, readable contrast, non-color status cues, and reduced-motion-compatible feedback. +Target WCAG 2.2 Level AA. All editor actions require keyboard access, visible focus, readable contrast, non-color status cues, and reduced-motion-compatible feedback. diff --git a/QUICK_START.md b/QUICK_START.md index 6a61fdb0..00198c0c 100644 --- a/QUICK_START.md +++ b/QUICK_START.md @@ -1,6 +1,6 @@ # Quick Start Guide -VeriWorkly is a monorepo consisting of a Next.js frontend and an Express backend. Follow these steps to get up and running quickly. +VeriWorkly is a monorepo consisting of 5 Next.js applications (Marketing Site, Studio, Portfolio Builder, Docs, Blog), an Express backend API, and a shared UI library. Follow these steps to get up and running quickly. ## ๐Ÿ“ฆ Installation @@ -8,7 +8,7 @@ VeriWorkly is a monorepo consisting of a Next.js frontend and an Express backend ```bash git clone https://github.com/VeriWorkly/veriworkly.git - cd veriworkly-resume + cd veriworkly ``` 2. **Install dependencies**: @@ -22,33 +22,36 @@ VeriWorkly is a monorepo consisting of a Next.js frontend and an Express backend ```bash cp .env.example .env cp apps/server/.env.example apps/server/.env + cp apps/studio/.env.example apps/studio/.env + cp apps/site/.env.example apps/site/.env + cp apps/portfolio/.env.example apps/portfolio/.env + cp apps/docs-platform/.env.example apps/docs-platform/.env + cp apps/blog-platform/.env.example apps/blog-platform/.env ``` 4. **Initialize the database**: + ```bash - npm run db:push -w @veriworkly/server + npm run db:push + npm run db:generate ``` -## ๐Ÿš€ Running the App +## ๐Ÿš€ Running the Apps -To start services (Site, Builder Studio, Backend, Docs, Blog) in development mode: +To start **all workspaces simultaneously** in development mode: ```bash -npm run dev -npm run dev:site -npm run dev:studio -npm run dev:server -npm run dev:docs -npm run dev:blog +npm run dev:all ``` -To start specific apps: +To run individual workspaces: -- **Site/Resume**: `npm run dev` (starts @veriworkly/site) -- **Studio**: `npm run dev:studio` -- **Server**: `npm run dev:server` -- **Docs**: `npm run dev:docs` -- **Blog**: `npm run dev:blog` +- **Marketing Site** (`http://localhost:3000`): `npm run dev:site` (or `npm run dev`) +- **Builder Studio** (`http://localhost:3001`): `npm run dev:studio` +- **Documentation** (`http://localhost:3002`): `npm run dev:docs` +- **Blog** (`http://localhost:3003`): `npm run dev:blog` +- **Portfolio Builder** (`http://localhost:3004`): `npm run dev:portfolio` +- **Backend API Server** (`http://localhost:8080`): `npm run dev:server` ## ๐Ÿ“š Detailed Documentation diff --git a/README.Local.md b/README.Local.md index 83f6dfe6..c17a20df 100644 --- a/README.Local.md +++ b/README.Local.md @@ -4,10 +4,10 @@ This guide covers setting up VeriWorkly for local development. ## Prerequisites -- **Node.js 20+** -- **npm v10+** +- **Node.js >= 20.19.0** (Node.js 22 supported) +- **npm v11+** (`npm@11.16.0`) - **PostgreSQL** (We recommend [Neon](https://neon.tech)) -- **Redis** (Local or via Docker) +- **Redis** (Local or via Docker; required for sessions and locks) ## Step-by-Step Setup @@ -18,20 +18,22 @@ This guide covers setting up VeriWorkly for local development. ``` 2. **Environment Configuration**: - - Create `.env` in the root. - - Create `.env` in `apps/server/`. - - See `ENV_SETUP.md` for variable details. + - Copy `.env.example` to `.env` in root and across `apps/` (see `ENV_SETUP.md`). -3. **Database Migration**: +3. **Database Setup**: ```bash - npm run db:push -w @veriworkly/server + npm run db:push + npm run db:generate ``` 4. **Start Development Servers**: ```bash - # Start everything - npm run dev + # Start all workspaces simultaneously + npm run dev:all + + # Or start marketing site only + npm run dev:site ``` ## ๐Ÿ“š Detailed Guide diff --git a/README.md b/README.md index 16c811d4..55bd48c7 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,11 @@ Instead of mandatory logins and data tracking, VeriWorkly operates on a **local- @@ -121,7 +121,7 @@ veriworkly/ ### Prerequisites -- **Node.js**: v20.0.0 or higher +- **Node.js**: v20.19.0 or higher (Node.js 22 supported) - **npm**: Repository uses npm workspaces (`npm install` from root) - **PostgreSQL**: Required for backend API functionality ([Neon](https://neon.tech) managed Postgres supported) - **Redis**: Required for backend sessions, rate limiting, quotas, and job locking @@ -200,6 +200,7 @@ npm test -w @veriworkly/server # Application unit & contract test suites (Vitest) npm run test:contracts -w @veriworkly/studio +npm run test:browser -w @veriworkly/studio npm run test:contracts -w @veriworkly/site npm test -w @veriworkly/portfolio diff --git a/apps/blog-platform/README.md b/apps/blog-platform/README.md index 98bf4191..616ad0f2 100644 --- a/apps/blog-platform/README.md +++ b/apps/blog-platform/README.md @@ -15,7 +15,7 @@ The official blog for VeriWorkly, built with Next.js and Fumadocs. npm run dev:blog ``` -The blog will be available at `http://localhost:3002`. +The blog will be available at `http://localhost:3003`. ## โœ๏ธ Content Management diff --git a/apps/blog-platform/content/blog/introducing-the-portfolio-builder.mdx b/apps/blog-platform/content/blog/introducing-the-portfolio-builder.mdx index 20179327..514952da 100644 --- a/apps/blog-platform/content/blog/introducing-the-portfolio-builder.mdx +++ b/apps/blog-platform/content/blog/introducing-the-portfolio-builder.mdx @@ -39,4 +39,4 @@ Free accounts publish at a shared path (`portfolio.veriworkly.com/portfolio/your The authenticated workspace โ€” dashboard, editor, analytics, settings โ€” is open to every logged-in user right now. You can build and preview a complete portfolio today, on any account. The one piece still opening up in stages is the publish button and checkout itself, as the launch path finishes getting validated end-to-end. Draft editing is unaffected either way. -Try it from your Studio dashboard โ€” the template gallery is public at veriworkly.com even before you sign in. +Try it from your Studio dashboard โ€” the portfolio template gallery is public at portfolio.veriworkly.com/templates even before you sign in. diff --git a/apps/blog-platform/package.json b/apps/blog-platform/package.json index 833b8a6e..272d0957 100644 --- a/apps/blog-platform/package.json +++ b/apps/blog-platform/package.json @@ -1,6 +1,6 @@ { "name": "@veriworkly/blog-platform", - "version": "3.24.0", + "version": "3.24.1", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/blog-platform/public/llms.txt b/apps/blog-platform/public/llms.txt index d645cd1a..5bc4afb2 100644 --- a/apps/blog-platform/public/llms.txt +++ b/apps/blog-platform/public/llms.txt @@ -90,6 +90,7 @@ These are labelled separately from career guidance and are written about VeriWor - [How the ATS Checker Actually Works](https://blog.veriworkly.com/how-the-ats-checker-actually-works) โ€” deterministic rules engine plus AI analysis layer, shared quota - [Introducing the Portfolio Builder](https://blog.veriworkly.com/introducing-the-portfolio-builder) - [GitHub vs LinkedIn Import: What's Real, What's AI](https://blog.veriworkly.com/github-vs-linkedin-import-whats-real-whats-ai) โ€” deterministic OAuth mapping versus AI text parsing +- [How Affiliate & Ambassador Rewards Actually Work](https://blog.veriworkly.com/how-affiliate-and-ambassador-rewards-actually-work) โ€” live 3-tier commission program, campus ambassador application status ## Citation Guidance for AI Agents Career articles on this subdomain are written to be cited independently of the product. diff --git a/apps/blog-platform/public/openapi.json b/apps/blog-platform/public/openapi.json index a1c8b444..ead69cc2 100644 --- a/apps/blog-platform/public/openapi.json +++ b/apps/blog-platform/public/openapi.json @@ -6,40 +6,23 @@ "description": "The official API reference for VeriWorkly.\n\n### Authentication Methods\n\nVeriWorkly supports two authentication methods:\n\n#### 1. API Key Authentication (recommended for scripts and integrations)\nFor programmatic access, send your key either way โ€” both are accepted:\n\n- `X-API-Key: vw_...`\n- `Authorization: Bearer vw_...`\n\nA key is **required** for any request that does not originate from a whitelisted VeriWorkly\norigin, including requests made from these documentation pages.\n\n- **Format**: `vw_` followed by 64 hexadecimal characters.\n- **Rate limit**: 20 requests per 15 minutes per key by default, configurable per key at\n creation. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and\n `X-RateLimit-Reset`.\n- **Scopes**: keys are scope-gated. A key missing a route's required scope gets `403`. Session\n callers are never scope-gated.\n\n#### 2. Session Authentication (dashboard only)\nUsed internally by the VeriWorkly dashboard and resume builder, via secure HTTP-only cookies\nissued by Better Auth.\n\n- **Cookies**:\n - Local: `veriworkly-auth.session_token`\n - Production: `__Secure-veriworkly-auth.session_token`\n- **Note**: sessions are only honoured for whitelisted origins. Send `credentials: 'include'`\n (fetch) or `withCredentials: true` (axios). Requests from these documentation pages **must**\n use an API key.\n\n#### Endpoints that need neither\nOnly `GET /api/v1/health` and `GET /api/v1/health/ready` are fully open. Everything else โ€”\nincluding the endpoints labelled \"public\", such as public portfolios and share links โ€” still\nrequires an API key or a whitelisted first-party origin, and returns `401` without one.\n\n### Response Envelope\n\nEvery endpoint under `/api/v1` except the Better Auth routes at `/api/v1/auth/*` returns\n`{ success, message, data }` on success and `{ success, message, statusCode, details? }` on\nfailure. Better Auth endpoints return their own payload shapes.\n" }, "servers": [ - { - "url": "https://api.veriworkly.com", - "description": "Production Server" - }, - { - "url": "http://localhost:8080", - "description": "Local Development" - } + { "url": "https://api.veriworkly.com", "description": "Production Server" }, + { "url": "http://localhost:8080", "description": "Local Development" } ], "tags": [ + { "name": "Health", "description": "Service health and readiness checks." }, + { "name": "Users", "description": "Operations related to authenticated users." }, + { "name": "Roadmap", "description": "Operations related to roadmap." }, { - "name": "Health", - "description": "Service health and readiness checks." - }, - { - "name": "Users", - "description": "Operations related to authenticated users." - }, - { - "name": "Roadmap", - "description": "Operations related to roadmap." - }, - { - "name": "GitHub", - "description": "GitHub issues, pull requests, and repository statistics." + "name": "Changelog", + "description": "Published release notes, filtering, and contributor statistics." }, + { "name": "GitHub", "description": "GitHub issues, pull requests, and repository statistics." }, { "name": "Authentication", "description": "Authentication operations using email OTP and sessions." }, - { - "name": "API Keys", - "description": "Operations related to managing personal API keys." - }, + { "name": "API Keys", "description": "Operations related to managing personal API keys." }, { "name": "Profile", "description": "The authenticated user's master profile โ€” the single source of truth new documents are seeded from." @@ -97,16 +80,11 @@ "description": "Server is healthy", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthLivenessResponse" - }, + "schema": { "$ref": "#/components/schemas/HealthLivenessResponse" }, "example": { "success": true, "message": "Server is healthy", - "data": { - "status": "ok", - "timestamp": "2026-04-26T10:00:00.000Z" - } + "data": { "status": "ok", "timestamp": "2026-04-26T10:00:00.000Z" } } } } @@ -125,9 +103,7 @@ "description": "Database and Redis are both reachable", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthReadyResponse" - }, + "schema": { "$ref": "#/components/schemas/HealthReadyResponse" }, "example": { "success": true, "message": "Server is healthy", @@ -145,14 +121,8 @@ "description": "The database or Redis could not be reached. The response carries no diagnostic detail โ€”\nthe underlying error is logged server-side only.\n", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 503, - "message": "Service Unavailable" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 503, "message": "Service Unavailable" } } } } @@ -163,17 +133,7 @@ "get": { "tags": ["Users"], "operationId": "getCurrentUser", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Current User", "description": "Retrieves account metadata for the currently authenticated user. Requires the `user:read`\nscope when called with an API key.\n\nThis returns identity metadata only โ€” resume, cover letter, and portfolio content is never\nexposed here. Results are cached for 30 minutes and refreshed on write.\n", "responses": { @@ -181,9 +141,7 @@ "description": "User information fetched successfully.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - }, + "schema": { "$ref": "#/components/schemas/UserResponse" }, "example": { "success": true, "message": "User information fetched successfully", @@ -196,31 +154,19 @@ "autoSyncEnabled": false, "createdAt": "2026-04-11T10:37:35.584Z", "updatedAt": "2026-04-25T19:35:44.409Z", - "_count": { - "apiKeys": 2, - "shareLinks": 4, - "resumes": 4 - } + "_count": { "apiKeys": 2, "shareLinks": 4, "resumes": 4 } } } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "User not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "User not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "User not found" } } } } @@ -231,17 +177,7 @@ "put": { "tags": ["Users"], "operationId": "updateUserName", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Update User Name", "description": "Updates the display name of the authenticated user. Requires the `user:write` scope when\ncalled with an API key.\n\nThe email address is read-only through this API. Leading and trailing whitespace is trimmed\nbefore validation.\n", "requestBody": { @@ -260,9 +196,7 @@ } } }, - "example": { - "name": "VeriWorkly" - } + "example": { "name": "VeriWorkly" } } } }, @@ -271,9 +205,7 @@ "description": "User name updated successfully.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - }, + "schema": { "$ref": "#/components/schemas/UserResponse" }, "example": { "success": true, "message": "User name updated successfully", @@ -286,11 +218,7 @@ "autoSyncEnabled": false, "createdAt": "2026-04-11T10:37:35.584Z", "updatedAt": "2026-04-26T02:00:00.000Z", - "_count": { - "apiKeys": 2, - "shareLinks": 4, - "resumes": 4 - } + "_count": { "apiKeys": 2, "shareLinks": 4, "resumes": 4 } } } } @@ -300,9 +228,7 @@ "description": "Validation error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "emptyName": { "summary": "Name is empty", @@ -310,12 +236,7 @@ "success": false, "statusCode": 400, "message": "Validation failed", - "details": [ - { - "path": "name", - "message": "Name cannot be empty" - } - ] + "details": [{ "path": "name", "message": "Name cannot be empty" }] } }, "nameTooLong": { @@ -324,49 +245,247 @@ "success": false, "statusCode": 400, "message": "Validation failed", - "details": [ - { - "path": "name", - "message": "Name is too long" - } - ] + "details": [{ "path": "name", "message": "Name is too long" }] } } } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" + "401": { "$ref": "#/components/responses/UnauthorizedError" } + } + } + }, + "/api/v1/users/me/username": { + "put": { + "tags": ["Users"], + "operationId": "setUsername", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "summary": "Set Username", + "description": "Claims the authenticated user's username. Requires the `user:write` scope when called with an\nAPI key.\n\nThe username is the public identifier in share-link URLs (`/share/{username}/{slug}`) and, for\naccounts holding `custom_subdomain`, the published-portfolio subdomain โ€” so a user cannot\ncreate a share link until one is set.\n\n**A username can only be set once.** Once the account has one, this endpoint rejects any\ndifferent value with `409 Username is locked and cannot be changed`. Re-sending the value the\naccount already holds is a no-op and returns `200`, so the call is safe to retry.\n\nThe submitted value is **normalised before validation**: lowercased, non-`[a-z0-9_-]`\ncharacters folded to hyphens, leading/trailing hyphens stripped, and truncated to 32\ncharacters. `Jane.Doe` and `jane-doe` therefore claim the same username. It must survive\nnormalisation as 3โ€“32 characters and must not be a reserved word (`admin`, `api`, `app`,\n`docs`, and similar platform-owned names). Check with\n`GET /users/{username}/availability` before committing.\n\nA successful claim invalidates the caller's cached session so the new username is visible\nimmediately rather than at the end of the session-cache TTL.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["username"], + "properties": { + "username": { + "type": "string", + "description": "Normalised server-side before the validity check runs." + } + } + }, + "example": { "username": "veriworkly-user" } + } + } + }, + "responses": { + "200": { + "description": "Username updated successfully.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UserResponse" }, + "example": { + "success": true, + "message": "Username updated successfully", + "data": { + "id": "cmnu7awv4000063lk18zpon2m", + "name": "VeriWorkly User", + "username": "veriworkly-user", + "email": "info@veriworkly.com", + "emailVerified": true, + "autoSyncEnabled": false, + "createdAt": "2026-04-11T10:37:35.584Z", + "updatedAt": "2026-04-26T02:00:00.000Z", + "_count": { "apiKeys": 2, "shareLinks": 4, "resumes": 4 } + } + } + } + } + }, + "400": { + "description": "The username failed validation after normalisation.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { + "success": false, + "statusCode": 400, + "message": "Validation failed", + "details": [{ "path": "username", "message": "invalid_username" }] + } + } + } + }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "409": { + "description": "The username is taken, or this account already has one.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "examples": { + "taken": { + "summary": "Claimed by another account", + "value": { + "success": false, + "statusCode": 409, + "message": "Username is already taken" + } + }, + "locked": { + "summary": "This account already has a username", + "value": { + "success": false, + "statusCode": 409, + "message": "Username is locked and cannot be changed" + } + } + } + } + } } } } }, - "/api/v1/roadmap": { - "get": { - "tags": ["Roadmap"], - "operationId": "getRoadmap", - "summary": "Get Roadmap Features", - "security": [ - { - "ApiKeyAuth": [] + "/api/v1/users/me/sync": { + "put": { + "tags": ["Users"], + "operationId": "updateAutoSync", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "summary": "Update Auto-Sync Preference", + "description": "Turns the account-level background document sync on or off. Requires the `user:write` scope\nwhen called with an API key.\n\nThis is the account-wide switch. It does not override a document's own `keepLocalOnly` flag โ€”\na document opted out individually stays local even with auto-sync on. Turning auto-sync off\ndoes not delete anything already synced; it only stops new local edits from being pushed.\n\nDefaults to `true` on a new account. A successful change invalidates the caller's cached\nsession.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["enabled"], + "properties": { "enabled": { "type": "boolean" } } + }, + "example": { "enabled": true } + } + } + }, + "responses": { + "200": { + "description": "Auto sync preference updated successfully.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UserResponse" }, + "example": { + "success": true, + "message": "Auto sync preference updated successfully", + "data": { + "id": "cmnu7awv4000063lk18zpon2m", + "name": "VeriWorkly User", + "username": "veriworkly-user", + "email": "info@veriworkly.com", + "emailVerified": true, + "autoSyncEnabled": true, + "createdAt": "2026-04-11T10:37:35.584Z", + "updatedAt": "2026-04-26T02:00:00.000Z", + "_count": { "apiKeys": 2, "shareLinks": 4, "resumes": 4 } + } + } + } + } }, - { - "LocalSession": [] + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { + "success": false, + "statusCode": 400, + "message": "Validation failed", + "details": [{ "path": "enabled", "message": "Expected boolean, received string" }] + } + } + } }, + "401": { "$ref": "#/components/responses/UnauthorizedError" } + } + } + }, + "/api/v1/users/{username}/availability": { + "get": { + "tags": ["Users"], + "operationId": "checkUsernameAvailability", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "summary": "Check Username Availability", + "description": "Reports whether a username can be claimed. Intended to drive live feedback in a username\npicker before `PUT /users/me/username` is called.\n\nThis route carries **no API-key scope gate**, unlike the rest of the Users module โ€” but it is\nstill not anonymous, so it needs an API key or a whitelisted first-party origin.\n\nThe path segment is normalised the same way the update endpoint normalises it, and\n`normalizedUsername` echoes back the value that would actually be stored. A `200` with\n`available: false` is the normal negative answer โ€” an unavailable name is not an error.\n", + "parameters": [ { - "ProdSession": [] + "name": "username", + "in": "path", + "required": true, + "schema": { "type": "string" }, + "description": "The candidate username. Normalised server-side before the lookup." } ], + "responses": { + "200": { + "description": "Username availability fetched successfully.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UsernameAvailabilityResponse" }, + "examples": { + "available": { + "summary": "Free to claim", + "value": { + "success": true, + "message": "Username availability fetched successfully", + "data": { "available": true, "normalizedUsername": "veriworkly-user" } + } + }, + "taken": { + "summary": "Already claimed", + "value": { + "success": true, + "message": "Username availability fetched successfully", + "data": { + "available": false, + "normalizedUsername": "veriworkly-user", + "reason": "taken" + } + } + }, + "invalid": { + "summary": "Fails the username rules", + "value": { + "success": true, + "message": "Username availability fetched successfully", + "data": { + "available": false, + "normalizedUsername": "a", + "reason": "too_short" + } + } + } + } + } + } + }, + "401": { "$ref": "#/components/responses/UnauthorizedError" } + } + } + }, + "/api/v1/roadmap": { + "get": { + "tags": ["Roadmap"], + "operationId": "getRoadmap", + "summary": "Get Roadmap Features", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "description": "Fetch roadmap features with optional filtering, sorting, and pagination.\n\nThis endpoint has no user session of its own: call it with an API key holding the\n`roadmap:read` scope, or from a whitelisted first-party origin. A request with neither is\nrejected with `401`.\n", "parameters": [ { "name": "status", "in": "query", - "schema": { - "type": "string", - "enum": ["todo", "in-progress", "done"] - }, + "schema": { "type": "string", "enum": ["todo", "in-progress", "done"] }, "required": false, "description": "Filter features by status" }, @@ -384,55 +503,35 @@ { "name": "limit", "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 20 - }, + "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "required": false, "description": "Items per page. Values above 50 are rejected with `400`." }, { "name": "offset", "in": "query", - "schema": { - "type": "integer", - "minimum": 0, - "default": 0 - }, + "schema": { "type": "integer", "minimum": 0, "default": 0 }, "required": false, "description": "Number of items to skip. Mutually redundant with `page` โ€” `page` wins if both are sent." }, { "name": "page", "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "default": 1 - }, + "schema": { "type": "integer", "minimum": 1, "default": 1 }, "required": false, "description": "1-based page index. An alternative to `offset`." }, { "name": "pageSize", "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50 - }, + "schema": { "type": "integer", "minimum": 1, "maximum": 50 }, "required": false, "description": "Alternative spelling of `limit`. Takes precedence over `limit` when both are sent." }, { "name": "excludeId", "in": "query", - "schema": { - "type": "string", - "minLength": 1 - }, + "schema": { "type": "string", "minLength": 1 }, "required": false, "description": "Omit one feature from the results โ€” useful for \"related features\" lists." } @@ -442,9 +541,7 @@ "description": "Roadmap features fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/RoadmapListResponse" - }, + "schema": { "$ref": "#/components/schemas/RoadmapListResponse" }, "example": { "success": true, "message": "Roadmap features fetched successfully", @@ -472,11 +569,7 @@ "pageSize": 20, "totalPages": 1, "hasMore": false, - "pagination": { - "mode": "offset", - "nextOffset": null, - "nextCursor": null - } + "pagination": { "mode": "offset", "nextOffset": null, "nextCursor": null } } } } @@ -486,18 +579,13 @@ "description": "Validation error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, "message": "Validation failed", "details": [ - { - "path": "limit", - "message": "Number must be greater than or equal to 1" - } + { "path": "limit", "message": "Number must be greater than or equal to 1" } ] } } @@ -511,26 +599,14 @@ "tags": ["Roadmap"], "operationId": "getRoadmapStats", "summary": "Get Roadmap Stats", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "description": "Retrieve aggregated roadmap statistics.", "responses": { "200": { "description": "Roadmap stats fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/RoadmapStatsResponse" - }, + "schema": { "$ref": "#/components/schemas/RoadmapStatsResponse" }, "example": { "success": true, "message": "Roadmap stats fetched successfully", @@ -553,26 +629,14 @@ "tags": ["Roadmap"], "operationId": "getRoadmapFeatureById", "summary": "Get Roadmap Feature by ID", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], - "description": "Fetch detailed roadmap feature by ID.", - "parameters": [ + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "description": "Fetch detailed roadmap feature by ID.", + "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "description": "Feature ID" } ], @@ -581,9 +645,7 @@ "description": "Feature fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/RoadmapFeatureResponse" - }, + "schema": { "$ref": "#/components/schemas/RoadmapFeatureResponse" }, "example": { "success": true, "message": "Feature fetched successfully", @@ -613,9 +675,7 @@ "description": "Feature not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -627,31 +687,255 @@ } } }, - "/api/v1/github/stats": { + "/api/v1/changelog": { "get": { - "tags": ["GitHub"], - "operationId": "getGitHubStats", - "summary": "Get GitHub Stats", - "security": [ + "tags": ["Changelog"], + "operationId": "getChangelogEntries", + "summary": "Get Changelog Entries", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "description": "Fetch published changelog entries, newest first, with optional filtering and pagination.\n\nThis endpoint has no user session of its own: call it with an API key holding the\n`changelog:read` scope, or from a whitelisted first-party origin. A request with neither is\nrejected with `401`.\n\nResults are cached per query for `CHANGELOG_CACHE_TTL_SECONDS` (default 30 days) and busted\nwhenever an admin writes an entry.\n", + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "schema": { "type": "string", "enum": ["major", "minor", "patch"] }, + "description": "Filter entries by release type." + }, + { + "name": "tag", + "in": "query", + "required": false, + "schema": { "type": "string", "minLength": 1 }, + "description": "Return only entries carrying this tag." + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { "type": "string", "minLength": 1, "maxLength": 120 }, + "description": "Case-insensitive substring match against the entry title and summary." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, + "description": "Items per page. Values above 50 are rejected with `400`." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { "type": "integer", "minimum": 0, "default": 0 }, + "description": "Number of items to skip. Redundant with `page` โ€” `page` wins if both are sent." + }, { - "ApiKeyAuth": [] + "name": "page", + "in": "query", + "required": false, + "schema": { "type": "integer", "minimum": 1, "default": 1 }, + "description": "1-based page index. An alternative to `offset`." }, { - "LocalSession": [] + "name": "pageSize", + "in": "query", + "required": false, + "schema": { "type": "integer", "minimum": 1, "maximum": 50 }, + "description": "Alternative spelling of `limit`. Takes precedence over `limit` when both are sent." + } + ], + "responses": { + "200": { + "description": "Changelog entries fetched successfully", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ChangelogListResponse" }, + "example": { + "success": true, + "message": "Changelog entries fetched successfully", + "data": { + "items": [ + { + "id": "v3-21-0", + "version": "3.24.0", + "title": "Portfolio template library refresh", + "summary": "Signal picks up a new hero and the editor gains section presets.", + "type": "minor", + "publishedAt": "2026-07-29T10:00:00.000Z", + "githubUrl": "https://github.com/VeriWorkly/veriworkly/releases/tag/v3.24.0", + "added": ["Section presets in the portfolio editor"], + "improved": ["Signal hero spacing on small viewports"], + "fixed": ["Duplicate toast on failed publish"], + "breaking": [], + "security": [], + "tags": ["portfolio", "editor"], + "prRefs": [ + { + "number": 412, + "title": "feat(portfolio): section presets", + "url": "https://github.com/VeriWorkly/veriworkly/pull/412", + "author": { + "login": "octocat", + "avatarUrl": "https://avatars.githubusercontent.com/u/583231", + "htmlUrl": "https://github.com/octocat" + } + } + ] + } + ], + "total": 1, + "limit": 20, + "offset": 0, + "page": 1, + "pageSize": 20, + "totalPages": 1, + "hasMore": false, + "pagination": { "mode": "offset", "nextOffset": null, "nextCursor": null } + } + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { + "success": false, + "statusCode": 400, + "message": "Validation failed", + "details": [ + { "path": "limit", "message": "Number must be less than or equal to 50" } + ] + } + } + } + }, + "401": { "$ref": "#/components/responses/UnauthorizedError" } + } + } + }, + "/api/v1/changelog/stats": { + "get": { + "tags": ["Changelog"], + "operationId": "getChangelogStats", + "summary": "Get Changelog Stats", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "description": "Aggregated release counts by type, the latest published release, and contributor totals\nderived from the pull-request references recorded on each entry.\n\nRequires the `changelog:read` scope when called with an API key.\n", + "responses": { + "200": { + "description": "Changelog stats fetched successfully", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ChangelogStatsResponse" }, + "example": { + "success": true, + "message": "Changelog stats fetched successfully", + "data": { + "totalEntries": 42, + "major": 3, + "minor": 18, + "patch": 21, + "latest": { + "version": "3.24.0", + "title": "Portfolio template library refresh", + "publishedAt": "2026-07-29T10:00:00.000Z" + }, + "contributorCount": 17, + "topContributors": [ + { + "login": "octocat", + "avatarUrl": "https://avatars.githubusercontent.com/u/583231", + "htmlUrl": "https://github.com/octocat", + "contributions": 9 + } + ] + } + } + } + } }, + "401": { "$ref": "#/components/responses/UnauthorizedError" } + } + } + }, + "/api/v1/changelog/{id}": { + "get": { + "tags": ["Changelog"], + "operationId": "getChangelogEntryById", + "summary": "Get Changelog Entry by ID", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], + "description": "Fetch a single changelog entry by its id. Requires the `changelog:read` scope when called\nwith an API key.\n", + "parameters": [ { - "ProdSession": [] + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "string" }, + "description": "The changelog entry id." } ], + "responses": { + "200": { + "description": "Changelog entry fetched successfully", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ChangelogEntryResponse" }, + "example": { + "success": true, + "message": "Changelog entry fetched successfully", + "data": { + "id": "v3-21-0", + "version": "3.24.0", + "title": "Portfolio template library refresh", + "summary": "Signal picks up a new hero and the editor gains section presets.", + "type": "minor", + "publishedAt": "2026-07-29T10:00:00.000Z", + "githubUrl": "https://github.com/VeriWorkly/veriworkly/releases/tag/v3.24.0", + "added": ["Section presets in the portfolio editor"], + "improved": ["Signal hero spacing on small viewports"], + "fixed": ["Duplicate toast on failed publish"], + "breaking": [], + "security": [], + "tags": ["portfolio", "editor"], + "prRefs": null + } + } + } + } + }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "404": { + "description": "Changelog entry not found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { + "success": false, + "statusCode": 404, + "message": "Changelog entry not found" + } + } + } + } + } + } + }, + "/api/v1/github/stats": { + "get": { + "tags": ["GitHub"], + "operationId": "getGitHubStats", + "summary": "Get GitHub Stats", + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "description": "Fetch aggregated issue and pull-request statistics for the configured repository, as of the\nlast sync. Served from the last synced snapshot, not live from GitHub โ€” `syncedAt` tells you\nhow fresh it is. Cached for 12 hours.\n\nThis endpoint has no user session of its own: call it with an API key holding the\n`github:read` scope, or from a whitelisted first-party origin. A request with neither is\nrejected with `401`.\n", "responses": { "200": { "description": "GitHub stats fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/GitHubStatsResponse" - }, + "schema": { "$ref": "#/components/schemas/GitHubStatsResponse" }, "examples": { "synced": { "summary": "Stats available", @@ -693,75 +977,42 @@ "tags": ["GitHub"], "operationId": "getGitHubIssues", "summary": "Get GitHub Issues", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "description": "Fetch paginated GitHub issues and pull requests with optional filters, from the last synced\nsnapshot rather than live from GitHub. Ordered by most recently updated. Returns an empty\npage when no sync has run yet.\n\nRequires the `github:read` scope when called with an API key.\n", "parameters": [ { "in": "query", "name": "status", - "schema": { - "type": "string", - "enum": ["todo", "in-progress", "done"] - }, + "schema": { "type": "string", "enum": ["todo", "in-progress", "done"] }, "description": "Derived at sync time โ€” closed items and items labelled `done` map to `done`; items\nlabelled `in-progress`, `in progress`, or `active` map to `in-progress`; everything else\nis `todo`.\n" }, { "in": "query", "name": "kind", - "schema": { - "type": "string", - "enum": ["issue", "pull-request", "all"] - }, + "schema": { "type": "string", "enum": ["issue", "pull-request", "all"] }, "description": "`all` is equivalent to omitting the filter." }, { "in": "query", "name": "limit", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 20 - }, + "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "description": "Items per page. Values above 50 are rejected with `400`." }, { "in": "query", "name": "offset", - "schema": { - "type": "integer", - "minimum": 0, - "default": 0 - } + "schema": { "type": "integer", "minimum": 0, "default": 0 } }, { "in": "query", "name": "page", - "schema": { - "type": "integer", - "minimum": 1, - "default": 1 - }, + "schema": { "type": "integer", "minimum": 1, "default": 1 }, "description": "1-based page index. An alternative to `offset`." }, { "in": "query", "name": "pageSize", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50 - }, + "schema": { "type": "integer", "minimum": 1, "maximum": 50 }, "description": "Alternative spelling of `limit`. Takes precedence over `limit` when both are sent." } ], @@ -770,9 +1021,7 @@ "description": "GitHub issues fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/GitHubIssuesResponse" - }, + "schema": { "$ref": "#/components/schemas/GitHubIssuesResponse" }, "example": { "success": true, "message": "GitHub issues fetched successfully", @@ -797,19 +1046,13 @@ "pageSize": 20, "totalPages": 1, "hasMore": false, - "pagination": { - "mode": "offset", - "nextOffset": null, - "nextCursor": null - } + "pagination": { "mode": "offset", "nextOffset": null, "nextCursor": null } } } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - } + "400": { "$ref": "#/components/responses/ValidationError" } } } }, @@ -817,14 +1060,7 @@ "post": { "tags": ["GitHub"], "operationId": "syncGitHubStats", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "(Admin) Sync GitHub", "description": "Manually triggers a GitHub sync. **Administrators only** โ€” the caller's session email must\nmatch the configured `ADMIN_EMAIL`; an API key cannot reach this route.\n\nIncremental by default: only issues updated since the last sync are refetched. The run is\nguarded by a 10-minute Redis lock, so a concurrent attempt fails with `409` rather than\ndouble-syncing. The response is the updated sync record.\n", "responses": { @@ -832,9 +1068,7 @@ "description": "Manual GitHub sync completed successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericSuccessResponse" - }, + "schema": { "$ref": "#/components/schemas/GenericSuccessResponse" }, "example": { "success": true, "message": "Manual GitHub sync completed successfully", @@ -855,21 +1089,13 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "The authenticated user is not the configured administrator.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 403, - "message": "Forbidden" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 403, "message": "Forbidden" } } } }, @@ -877,9 +1103,7 @@ "description": "A sync is already running.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 409, @@ -892,9 +1116,7 @@ "description": "The GitHub API could not be reached, or kept returning a retryable error.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 502, @@ -920,10 +1142,7 @@ "type": "object", "required": ["email", "type"], "properties": { - "email": { - "type": "string", - "format": "email" - }, + "email": { "type": "string", "format": "email" }, "type": { "type": "string", "enum": ["sign-in", "email-verification", "forget-password"], @@ -931,10 +1150,7 @@ } } }, - "example": { - "email": "user@example.com", - "type": "sign-in" - } + "example": { "email": "user@example.com", "type": "sign-in" } } } }, @@ -943,26 +1159,13 @@ "description": "OTP sent. Returns `{ \"success\": true }` whether or not the address maps to an existing\naccount, so this endpoint cannot be used to enumerate registered emails.\n", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - }, - "example": { - "success": true - } + "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } }, + "example": { "success": true } } } }, - "400": { - "description": "Missing or malformed email address." - }, - "429": { - "description": "Too many OTP requests โ€” 3 per minute per IP." - } + "400": { "description": "Missing or malformed email address." }, + "429": { "description": "Too many OTP requests โ€” 3 per minute per IP." } } } }, @@ -980,19 +1183,11 @@ "type": "object", "required": ["email", "otp"], "properties": { - "email": { - "type": "string", - "format": "email" - }, - "otp": { - "type": "string" - } + "email": { "type": "string", "format": "email" }, + "otp": { "type": "string" } } }, - "example": { - "email": "user@example.com", - "otp": "123456" - } + "example": { "email": "user@example.com", "otp": "123456" } } } }, @@ -1009,10 +1204,7 @@ "type": "string", "description": "Session token for the authenticated session." }, - "user": { - "type": "object", - "additionalProperties": true - } + "user": { "type": "object", "additionalProperties": true } } } } @@ -1021,9 +1213,7 @@ "400": { "description": "The OTP is invalid, expired, or the attempt limit was exceeded." }, - "429": { - "description": "Too many requests โ€” 20 per minute per IP." - } + "429": { "description": "Too many requests โ€” 20 per minute per IP." } } } }, @@ -1032,14 +1222,7 @@ "tags": ["Authentication"], "operationId": "getSession", "summary": "Get Current Session", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "description": "Returns the active session and its user, based on the request's session cookie. Returns\n`null` rather than an error when no valid session cookie is present.\n\nThis endpoint returns Better Auth's payload, not the `{ success, message, data }` envelope.\n", "responses": { "200": { @@ -1051,19 +1234,11 @@ { "type": "object", "properties": { - "session": { - "type": "object", - "additionalProperties": true - }, - "user": { - "type": "object", - "additionalProperties": true - } + "session": { "type": "object", "additionalProperties": true }, + "user": { "type": "object", "additionalProperties": true } } }, - { - "type": "null" - } + { "type": "null" } ] } } @@ -1077,31 +1252,15 @@ "tags": ["Authentication"], "operationId": "signOut", "summary": "Sign Out", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "description": "Revokes the current session and clears the session cookie. Also flushes the server-side\nsession cache for that cookie.\n\nThis endpoint returns Better Auth's payload, not the `{ success, message, data }` envelope.\n", "responses": { "200": { "description": "Signed out. Clears the session cookie.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - }, - "example": { - "success": true - } + "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } }, + "example": { "success": true } } } } @@ -1114,56 +1273,32 @@ "operationId": "getAllApiKeys", "summary": "List API Keys", "description": "Lists the caller's API keys, newest first.\n\n**No form of the secret is returned here โ€” not even a masked one.** Each entry carries only\n`keyPrefix`, the first 8 characters of the key, which is enough to tell keys apart in a list.\nIf you lose a secret, rotate the key.\n\nSession-authenticated only โ€” an API key cannot be used to manage API keys.\n", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "parameters": [ { "name": "limit", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 20 - } + "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 } }, { "name": "offset", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0 - } + "schema": { "type": "integer", "minimum": 0, "default": 0 } }, { "name": "page", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "default": 1 - }, + "schema": { "type": "integer", "minimum": 1, "default": 1 }, "description": "1-based page index. An alternative to `offset`." }, { "name": "pageSize", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50 - }, + "schema": { "type": "integer", "minimum": 1, "maximum": 50 }, "description": "Alternative spelling of `limit`. Takes precedence over `limit` when both are sent." } ], @@ -1172,9 +1307,7 @@ "description": "A list of API keys.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyListResponse" - }, + "schema": { "$ref": "#/components/schemas/ApiKeyListResponse" }, "example": { "success": true, "message": "API keys fetched successfully", @@ -1196,19 +1329,13 @@ "pageSize": 20, "totalPages": 1, "hasMore": false, - "pagination": { - "mode": "offset", - "nextOffset": null, - "nextCursor": null - } + "pagination": { "mode": "offset", "nextOffset": null, "nextCursor": null } } } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } }, "post": { @@ -1216,14 +1343,7 @@ "operationId": "createApiKey", "summary": "Create API Key", "description": "Generates a new API key. **The full secret is returned exactly once, in this response.** It\nis stored only as an HMAC-SHA256 hash and can never be retrieved again โ€” save it immediately.\n\nSession-authenticated only โ€” an API key cannot be used to mint API keys.\n", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "requestBody": { "required": true, "content": { @@ -1241,9 +1361,7 @@ "scopes": { "type": "array", "description": "Scopes granted to the new key. Defaults to `[\"user:read\"]` when omitted. Any\nvalue outside the listed set is rejected with `400`.\n", - "items": { - "$ref": "#/components/schemas/ApiKeyScope" - } + "items": { "$ref": "#/components/schemas/ApiKeyScope" } }, "rateLimit": { "type": "integer", @@ -1259,10 +1377,7 @@ } } }, - "example": { - "name": "My Documentation Key", - "scopes": ["user:read", "resume:read"] - } + "example": { "name": "My Documentation Key", "scopes": ["user:read", "resume:read"] } } } }, @@ -1271,9 +1386,7 @@ "description": "API key created successfully.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyCreateResponse" - }, + "schema": { "$ref": "#/components/schemas/ApiKeyCreateResponse" }, "example": { "success": true, "message": "API key generated successfully. Please save it as it won't be shown again.", @@ -1301,9 +1414,7 @@ "description": "Missing name, an unsupported scope, or a malformed `rateLimit`/`expiresAt`.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingName": { "summary": "Name omitted", @@ -1325,9 +1436,7 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -1337,22 +1446,13 @@ "operationId": "rotateApiKey", "summary": "Rotate API Key", "description": "Revokes the target key and issues a replacement in one transaction. The old secret stops\nworking immediately.\n\nEvery field in the body is optional โ€” send `{}` to rotate the secret while carrying over the\nexisting name, scopes, and rate limit. As with creation, the new secret is returned exactly\nonce. The response also carries `rotatedFromId`, the id of the key that was replaced.\n\nOnly an active, unrevoked key can be rotated; anything else returns `404`.\n", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "description": "The ID of the API key to rotate." } ], @@ -1370,9 +1470,7 @@ "scopes": { "type": "array", "description": "Scopes for the replacement key. Carried over from the old key when omitted.", - "items": { - "$ref": "#/components/schemas/ApiKeyScope" - } + "items": { "$ref": "#/components/schemas/ApiKeyScope" } }, "rateLimit": { "type": "integer", @@ -1396,9 +1494,7 @@ "description": "API key rotated successfully.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyCreateResponse" - }, + "schema": { "$ref": "#/components/schemas/ApiKeyCreateResponse" }, "example": { "success": true, "message": "API key rotated successfully. Please save the new key immediately.", @@ -1426,23 +1522,15 @@ "400": { "description": "An unsupported scope, or a malformed `rateLimit`/`expiresAt`.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "No active key with this ID belongs to the caller.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -1460,22 +1548,13 @@ "operationId": "revokeApiKey", "summary": "Revoke API Key", "description": "Immediately disables an API key without deleting its record โ€” `isActive` becomes false and\n`revokedAt` is stamped. The cached authentication record is purged at the same time, so the\nsecret stops working right away rather than after the cache expires.\n\nA revoked key cannot be reactivated; rotate or create a new one instead. `data` is `null` on\nsuccess.\n", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "description": "The ID of the API key to revoke." } ], @@ -1484,9 +1563,7 @@ "description": "API key revoked successfully.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/NullDataResponse" - }, + "schema": { "$ref": "#/components/schemas/NullDataResponse" }, "example": { "success": true, "message": "API key revoked successfully", @@ -1495,21 +1572,13 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "No key with this ID belongs to the caller.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "API key not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "API key not found" } } } } @@ -1517,27 +1586,73 @@ } }, "/api/v1/api-keys/{id}": { + "get": { + "tags": ["API Keys"], + "operationId": "getApiKey", + "summary": "Get API Key", + "description": "Returns the full metadata record for one of the caller's keys โ€” name, scopes, rate limit,\nexpiry, revocation state, and last-used timestamp.\n\n**The secret is not returned**, and cannot be: only an HMAC-SHA256 hash of it is stored. If\nyou have lost a key's value, rotate it. `keyPrefix` and `keySuffix` are the only fragments\nkept, and neither is enough to reconstruct the key.\n\nSession-authenticated only โ€” an API key cannot be used to manage API keys.\n", + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "string" }, + "description": "The ID of the API key to fetch." + } + ], + "responses": { + "200": { + "description": "API key fetched successfully.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ApiKeyDetailResponse" }, + "example": { + "success": true, + "message": "API key fetched successfully", + "data": { + "id": "cmnu7bxq2000163lk4d1zqp8n", + "keyPrefix": "vw_a1b2c", + "keySuffix": "9f8e7d6", + "name": "CI pipeline", + "userId": "cmnu7awv4000063lk18zpon2m", + "isActive": true, + "rateLimit": 20, + "scopes": ["user:read", "resume:read"], + "expiresAt": "2027-04-11T10:37:35.584Z", + "revokedAt": null, + "createdAt": "2026-04-11T10:37:35.584Z", + "updatedAt": "2026-04-11T10:37:35.584Z", + "lastUsed": "2026-07-29T09:55:00.000Z" + } + } + } + } + }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "404": { + "description": "No key with this ID belongs to the caller.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "API key not found" } + } + } + } + } + }, "delete": { "tags": ["API Keys"], "operationId": "deleteApiKey", "summary": "Delete API Key", "description": "Permanently removes an API key and its record. Use `POST /api-keys/{id}/revoke` instead if\nyou want to keep the audit trail.\n\n`data` is `null` on success.\n", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "description": "The ID of the API key to delete." } ], @@ -1546,9 +1661,7 @@ "description": "API key deleted successfully.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/NullDataResponse" - }, + "schema": { "$ref": "#/components/schemas/NullDataResponse" }, "example": { "success": true, "message": "API key deleted successfully", @@ -1557,21 +1670,13 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "No key with this ID belongs to the caller.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "API key not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "API key not found" } } } } @@ -1582,17 +1687,7 @@ "get": { "tags": ["Profile"], "operationId": "getMasterProfile", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Master Profile", "description": "Retrieves the authenticated user's master profile along with an account summary. Requires the\n`resume:read` scope when called with an API key.\n\nWhen the user has never saved a profile, this still returns `200` with a placeholder whose\n`id` is an empty string and whose `content` is `{}` โ€” it does not return `404`. Cached for\n1 hour and invalidated on write.\n", "responses": { @@ -1600,9 +1695,7 @@ "description": "Master profile fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/MasterProfileResponse" - }, + "schema": { "$ref": "#/components/schemas/MasterProfileResponse" }, "examples": { "success": { "summary": "Successful response", @@ -1633,21 +1726,13 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "User not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "User not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "User not found" } } } } @@ -1656,26 +1741,14 @@ "put": { "tags": ["Profile"], "operationId": "updateMasterProfile", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Update Master Profile", "description": "Replaces the authenticated user's master profile. Requires the `resume:write` scope when\ncalled with an API key.\n\n**This is a full replacement, not a patch.** `profile` is validated strictly: every property\nof `MasterProfileContent` must be present, and any key not in the schema is rejected. Send\nempty arrays for sections you have no data for. Read the profile first, modify the object you\ngot back, and send the whole thing.\n\nPass the `updatedAt` you last read as `expectedUpdatedAt` for optimistic concurrency; omit it\nonly when creating the profile for the first time. The example below is a complete minimal\npayload that validates.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/MasterProfileUpdateRequest" - }, + "schema": { "$ref": "#/components/schemas/MasterProfileUpdateRequest" }, "example": { "expectedUpdatedAt": "2026-04-10T10:00:00.000Z", "profile": { @@ -1691,10 +1764,7 @@ "linkPhone": false, "linkLocation": false }, - "links": { - "displayMode": "icon", - "items": [] - }, + "links": { "displayMode": "icon", "items": [] }, "summary": "Backend engineer focused on distributed systems.", "experience": [], "education": [], @@ -1734,25 +1804,17 @@ "description": "Master profile updated successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/MasterProfileUpdateResponse" - } + "schema": { "$ref": "#/components/schemas/MasterProfileUpdateResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "409": { "description": "Conflict - profile updated elsewhere", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 409, @@ -1765,9 +1827,7 @@ "description": "Payload too large", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 413, @@ -1783,17 +1843,7 @@ "get": { "tags": ["Profile Import"], "operationId": "getImportQuota", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Profile Import Quota", "description": "Returns the caller's current GitHub and LinkedIn import quota status in one call.\n\n- **GitHub import** is a real GitHub OAuth-backed integration (up to 30 of the account's most\n recently updated repositories, deterministically mapped to skills by repository language โ€”\n no AI is involved). Free users get 1 import/day and may only import their own OAuth-connected\n GitHub account. Paid users (any active AI Credits or Portfolio Pro/Bundle plan) may import\n any public GitHub username, but are still capped at a hidden 50 imports/day โ€” this protects\n the single shared `config.github.token` request budget used by every paid import and the\n GitHub sync cron job from being exhausted by one account.\n- **LinkedIn import** is not a LinkedIn API integration โ€” it's an AI parse of raw text the\n user pastes in (copied profile text, or text extracted from an exported PDF). Free users get\n 1 import/month; paid users get unlimited imports.\n\nBoth import types share a single `replaceMaster` request flag and always create a brand-new\nRESUME document from the imported data, in addition to optionally overwriting the master\nprofile.\n", "responses": { @@ -1801,9 +1851,7 @@ "description": "Quota status fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ProfileImportQuotaResponse" - }, + "schema": { "$ref": "#/components/schemas/ProfileImportQuotaResponse" }, "example": { "success": true, "message": "Quota status fetched successfully", @@ -1827,9 +1875,7 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -1837,30 +1883,15 @@ "post": { "tags": ["Profile Import"], "operationId": "importGithub", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Import Profile From GitHub", "description": "Imports a GitHub profile and repositories directly via the GitHub REST API (no AI parsing) and\ncreates a new RESUME document from the result. Free users can only import their own\nOAuth-connected GitHub account (`usernameOrUrl` is ignored in that case); paid users may import\nany public username, subject to the hidden 50/day shared-token cap described on\n`GET /profiles/import/quota`.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ImportGithubRequest" - }, - "example": { - "usernameOrUrl": "octocat", - "replaceMaster": false - } + "schema": { "$ref": "#/components/schemas/ImportGithubRequest" }, + "example": { "usernameOrUrl": "octocat", "replaceMaster": false } } } }, @@ -1869,9 +1900,7 @@ "description": "GitHub profile imported successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ProfileImportResponse" - }, + "schema": { "$ref": "#/components/schemas/ProfileImportResponse" }, "example": { "success": true, "message": "GitHub profile imported successfully", @@ -1898,9 +1927,7 @@ "description": "Free user has no connected GitHub account, or a validation error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -1909,16 +1936,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "GitHub profile not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -1931,9 +1954,7 @@ "description": "Import quota exceeded", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "freeDailyLimit": { "summary": "Free-tier daily limit", @@ -1962,26 +1983,14 @@ "post": { "tags": ["Profile Import"], "operationId": "importLinkedin", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Import Profile From LinkedIn", "description": "Parses raw copy-pasted LinkedIn profile text (or text extracted from an exported PDF) using AI\nand creates a new RESUME document from the result. This is an AI-parsed paste, not a LinkedIn\nAPI integration. Free users get 1 import/month; paid users (AI Credits or Portfolio Pro/Bundle)\nget unlimited imports.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ImportLinkedinRequest" - }, + "schema": { "$ref": "#/components/schemas/ImportLinkedinRequest" }, "example": { "profileText": "Jane Doe\nSoftware Engineer at Acme Corp\n...", "replaceMaster": false @@ -1994,9 +2003,7 @@ "description": "LinkedIn profile imported successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ProfileImportResponse" - }, + "schema": { "$ref": "#/components/schemas/ProfileImportResponse" }, "example": { "success": true, "message": "LinkedIn profile imported successfully", @@ -2023,9 +2030,7 @@ "description": "Validation error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -2040,16 +2045,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "429": { "description": "Import quota exceeded", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 429, @@ -2062,9 +2063,7 @@ "description": "AI parsing failed", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 502, @@ -2080,17 +2079,7 @@ "get": { "tags": ["Documents"], "operationId": "listDocuments", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "List Documents", "description": "Lists the authenticated user's documents (resumes, cover letters, portfolios, link-in-bio\npages), optionally filtered by type. Soft-deleted documents are excluded.\n", "parameters": [ @@ -2110,9 +2099,7 @@ "description": "Documents fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentListResponse" - }, + "schema": { "$ref": "#/components/schemas/DocumentListResponse" }, "example": { "success": true, "message": "Documents fetched successfully", @@ -2140,41 +2127,22 @@ } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" } } }, "post": { "tags": ["Documents"], "operationId": "createDocument", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Create Document", "description": "Creates a new document. If no `content` is provided for a RESUME or COVER_LETTER, it is\nauto-seeded from the user's MasterProfile (when one exists). Free-tier users are limited to\n1 active document per type; upgrading removes that limit.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentCreateRequest" - }, - "example": { - "type": "RESUME", - "title": "My Resume" - } + "schema": { "$ref": "#/components/schemas/DocumentCreateRequest" }, + "example": { "type": "RESUME", "title": "My Resume" } } } }, @@ -2182,26 +2150,16 @@ "201": { "description": "Document created successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/DocumentResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "Free-tier document limit reached", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 403, @@ -2214,9 +2172,7 @@ "description": "Payload too large", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 413, @@ -2232,54 +2188,25 @@ "get": { "tags": ["Documents"], "operationId": "getDocument", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Document", "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Document fetched successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/DocumentResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Document not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Document not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Document not found" } } } } @@ -2288,40 +2215,18 @@ "patch": { "tags": ["Documents"], "operationId": "updateDocument", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Update Document", "description": "Updates a document using optimistic concurrency control: `revision` must match the\ndocument's current revision or the request fails with `409`.\n", "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentUpdateRequest" - }, - "example": { - "title": "Updated Resume Title", - "revision": 1 - } + "schema": { "$ref": "#/components/schemas/DocumentUpdateRequest" }, + "example": { "title": "Updated Resume Title", "revision": 1 } } } }, @@ -2329,31 +2234,17 @@ "200": { "description": "Document updated successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/DocumentResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Document not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Document not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Document not found" } } } }, @@ -2361,9 +2252,7 @@ "description": "Revision mismatch, or slug conflict", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "revisionMismatch": { "summary": "Optimistic concurrency conflict", @@ -2389,9 +2278,7 @@ "description": "Payload too large", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 413, @@ -2405,37 +2292,18 @@ "delete": { "tags": ["Documents"], "operationId": "deleteDocument", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Delete Document", "description": "Soft-deletes a document by setting `deletedAt`. The document is immediately excluded from\nlist/get responses. Hard delete and restore exist at the service layer but are not exposed\nthrough any public route.\n\n`data` is `null` on success โ€” the deleted document is not echoed back.\n", "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Document deleted successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/NullDataResponse" - }, + "schema": { "$ref": "#/components/schemas/NullDataResponse" }, "example": { "success": true, "message": "Document deleted successfully", @@ -2444,21 +2312,13 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Document not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Document not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Document not found" } } } } @@ -2469,17 +2329,7 @@ "get": { "tags": ["ATS"], "operationId": "getAtsQuota", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get ATS Scan Quota", "description": "Returns the caller's current ATS scan quota status. This endpoint works for anonymous\ncallers too (quota is keyed by a hashed IP when unauthenticated).\n\nThere is a single shared scan-quota bucket for both the deterministic `/ats/check` scan and\nthe AI-powered `/ats/analyze` deep analysis โ€” they are **not** two separate quotas. Limits:\n\n- Anonymous: 1 scan / 48 hours\n- Free logged-in: 2 scans / 24 hours\n- Subscriber (active AI Credits or Bundle plan): 300 scans / billing period\n", "responses": { @@ -2487,9 +2337,7 @@ "description": "Quota fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsQuotaResponse" - }, + "schema": { "$ref": "#/components/schemas/AtsQuotaResponse" }, "example": { "success": true, "message": "Success", @@ -2501,21 +2349,11 @@ "resetsAt": "2026-04-28T00:00:00.000Z", "canConvertResume": false, "pricing": { - "analysisCredits": { - "min": 1, - "max": 6 - }, - "jobUrlAnalysisCredits": { - "min": 2, - "max": 12 - }, + "analysisCredits": { "min": 1, "max": 6 }, + "jobUrlAnalysisCredits": { "min": 2, "max": 12 }, "resumeConversionCredits": 2 }, - "extract": { - "limit": 6, - "used": 0, - "remaining": 6 - } + "extract": { "limit": 6, "used": 0, "remaining": 6 } } } } @@ -2528,17 +2366,7 @@ "post": { "tags": ["ATS"], "operationId": "extractAtsResumeText", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Extract Resume Text", "description": "Extracts plain text from an uploaded resume file (PDF, DOCX, TXT, Markdown, or JSON, max 5MB)\nfor use as the `resume` field on `/ats/check` or `/ats/analyze`. Does not consume ATS scan\nquota โ€” only the check/analyze endpoints do.\n", "requestBody": { @@ -2548,12 +2376,7 @@ "schema": { "type": "object", "required": ["resume"], - "properties": { - "resume": { - "type": "string", - "format": "binary" - } - } + "properties": { "resume": { "type": "string", "format": "binary" } } } } } @@ -2563,9 +2386,7 @@ "description": "Text extracted successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsExtractResponse" - } + "schema": { "$ref": "#/components/schemas/AtsExtractResponse" } } } }, @@ -2573,9 +2394,7 @@ "description": "Missing file, or unsupported/unreadable file", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingFile": { "summary": "No file provided", @@ -2601,9 +2420,7 @@ "description": "Extraction took too long", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 408, @@ -2619,26 +2436,14 @@ "post": { "tags": ["ATS"], "operationId": "checkAtsResume", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Run Deterministic ATS Scan", "description": "Runs the free, rule-based ATS readiness scan โ€” no AI call is made. Works for anonymous\ncallers. Consumes one unit from the shared ATS scan quota (see `GET /ats/quota`).\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsCheckRequest" - }, + "schema": { "$ref": "#/components/schemas/AtsCheckRequest" }, "example": { "resume": "Jane Doe\nSoftware Engineer\n...", "jobDescription": "We are looking for a Software Engineer with React experience..." @@ -2650,23 +2455,15 @@ "200": { "description": "Scan completed successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsCheckResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/AtsCheckResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, "429": { "description": "ATS scan quota exceeded", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 429, @@ -2682,26 +2479,14 @@ "post": { "tags": ["ATS"], "operationId": "analyzeAtsResume", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Run AI-Powered ATS Analysis", "description": "Runs the deterministic scan plus an AI deep-analysis pass on top of it (explanation, missing\nevidence, keyword opportunities, prioritized fixes). Requires authentication. Consumes one\nunit from the same shared ATS scan quota as `/ats/check` โ€” this is not a second, separate\nquota. Requires the `ai:write` API key scope when called with an API key.\n\nOptionally fetches a job description from a URL instead of requiring pasted text\n(`fetchJobUrl: true` + `jobUrl`). The fetch is SSRF-hardened: HTTPS-only on the standard port,\nrejects localhost/private/link-local addresses, resolves DNS once and pins that IP for the\nwhole request (including redirects), and caps redirects to 3, response size to 2MB, and the\nrequest timeout to 8 seconds.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsAnalyzeRequest" - }, + "schema": { "$ref": "#/components/schemas/AtsAnalyzeRequest" }, "example": { "resume": "Jane Doe\nSoftware Engineer\n...", "jobDescription": "We are looking for a Software Engineer with React experience...", @@ -2716,9 +2501,7 @@ "description": "Analysis completed successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsAnalyzeResponse" - } + "schema": { "$ref": "#/components/schemas/AtsAnalyzeResponse" } } } }, @@ -2726,17 +2509,11 @@ "description": "Validation error, or job URL fetch was blocked", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "validation": { "summary": "Validation error", - "value": { - "success": false, - "statusCode": 400, - "message": "Validation failed" - } + "value": { "success": false, "statusCode": 400, "message": "Validation failed" } }, "blockedUrl": { "summary": "Job URL resolves to a blocked network", @@ -2750,16 +2527,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "402": { "description": "Not enough AI credits", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 402, @@ -2772,9 +2545,7 @@ "description": "API key is missing the ai:write scope", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 403, @@ -2787,9 +2558,7 @@ "description": "ATS scan quota exceeded", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 429, @@ -2802,9 +2571,7 @@ "description": "AI provider error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 502, @@ -2820,26 +2587,14 @@ "post": { "tags": ["ATS"], "operationId": "convertAtsResume", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Convert Resume Text To Structured JSON", "description": "Uses AI to convert raw resume text into structured JSON Resume-shaped fields. Requires an\nactive AI Credits or Bundle entitlement, and spends credits independently of the shared ATS\nscan quota above. Requires the `ai:write` API key scope when called with an API key.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsConvertResumeRequest" - }, + "schema": { "$ref": "#/components/schemas/AtsConvertResumeRequest" }, "example": { "resume": "Jane Doe\nSoftware Engineer\n...", "requestId": "ats-convert-8f3ac1e2b9" @@ -2852,25 +2607,17 @@ "description": "Resume converted successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AtsConvertResumeResponse" - } + "schema": { "$ref": "#/components/schemas/AtsConvertResumeResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "402": { "description": "Not enough AI credits", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 402, @@ -2883,9 +2630,7 @@ "description": "Missing entitlement, or API key is missing the ai:write scope", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 403, @@ -2898,9 +2643,7 @@ "description": "AI provider error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 502, @@ -2916,40 +2659,22 @@ "get": { "tags": ["AI"], "operationId": "getAiActions", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "List AI Actions", "description": "Lists every available AI generation action along with its Standard and Expert mode credit\ncosts. Use this to build pricing UI before calling `/ai/generate`.\n\nCosts only โ€” no user data is returned, and no credits are spent. This endpoint carries no\nscope requirement, but it is not open to the public: the request must carry an API key or\noriginate from a whitelisted first-party origin, or it is rejected with `401`.\n\nThe response always contains an entry for all seven action keys. **Treat this endpoint as the\nonly source of truth for pricing** โ€” the actual credit values come from server-side\nconfiguration and can change without a documentation update, so read them at runtime rather\nthan hardcoding the illustrative numbers shown here.\n", "responses": { "200": { "description": "Actions fetched successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AiActionsResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/AiActionsResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "503": { "description": "The AI policy is missing or invalid on the server.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 503, @@ -2965,26 +2690,14 @@ "post": { "tags": ["AI"], "operationId": "generateAiContent", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Generate AI Content", "description": "Runs an AI generation action (rewriting a section, generating a cover letter, tailoring a\nresume to a job description, etc). Requires the `ai:write` scope when called with an API key.\n\nCredits are handled with a two-phase reserve โ†’ commit/release flow: the cost for the chosen\naction + mode is reserved from the caller's AI credit wallet before the AI provider is called,\nthen committed only after a successful response. If the AI call fails or errors out, the\nreservation is released instead of committed โ€” a failed generation never burns credits.\n\nExpert mode uses a stronger, more expensive model than Standard mode for the same action.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AiGenerateRequest" - }, + "schema": { "$ref": "#/components/schemas/AiGenerateRequest" }, "example": { "action": "generate_cover_letter", "mode": "standard", @@ -3004,23 +2717,14 @@ "description": "Content generated successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AiGenerateResponse" - }, + "schema": { "$ref": "#/components/schemas/AiGenerateResponse" }, "example": { "success": true, "message": "Success", "data": { "content": "Dear Hiring Manager, ...", - "usage": { - "promptTokens": 420, - "completionTokens": 280, - "totalTokens": 700 - }, - "credits": { - "spent": 4, - "balance": 46 - } + "usage": { "promptTokens": 420, "completionTokens": 280, "totalTokens": 700 }, + "credits": { "spent": 4, "balance": 46 } } } } @@ -3030,9 +2734,7 @@ "description": "Validation error, or text/context exceeds the action's character limit", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -3041,16 +2743,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "402": { "description": "Not enough AI credits", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 402, @@ -3063,9 +2761,7 @@ "description": "API key is missing the ai:write scope", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 403, @@ -3078,9 +2774,7 @@ "description": "requestId was already used, or is still processing", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 409, @@ -3093,9 +2787,7 @@ "description": "AI provider error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 502, @@ -3113,32 +2805,19 @@ "operationId": "listPublicPortfolios", "summary": "List Public Portfolios", "description": "Lists the subdomains of every portfolio currently in `LIVE` or `GRACE` status, most recently\nupdated first. Returns subdomain and timestamp only โ€” no portfolio content.\n\nNo user session is required, but the request must carry an API key or originate from a\nwhitelisted first-party origin; otherwise it is rejected with `401`. Cached for 5 minutes.\n", - "security": [ - { - "ApiKeyAuth": [] - }, - {} - ], + "security": [{ "ApiKeyAuth": [] }, {}], "parameters": [ { "name": "limit", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } + "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }, { "name": "offset", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "maximum": 5000 - } + "schema": { "type": "integer", "minimum": 0, "maximum": 5000 } } ], "responses": { @@ -3146,15 +2825,11 @@ "description": "Portfolios fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioPublicListResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioPublicListResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - } + "400": { "$ref": "#/components/responses/ValidationError" } } } }, @@ -3164,30 +2839,16 @@ "operationId": "getPublicPortfolio", "summary": "Get Public Portfolio By Subdomain", "description": "Returns the published snapshot for a live portfolio subdomain.\n\nReturns `404` for a suspended portfolio, or one whose grace period has just expired โ€” that\nlookup also flips the record to `SUSPENDED` as a side effect. Cached for up to 10 minutes,\nshortened to the remaining grace window for portfolios in `GRACE`.\n\nNo user session is required, but the request must carry an API key or originate from a\nwhitelisted first-party origin; otherwise it is rejected with `401`.\n", - "security": [ - { - "ApiKeyAuth": [] - }, - {} - ], + "security": [{ "ApiKeyAuth": [] }, {}], "parameters": [ - { - "name": "subdomain", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "subdomain", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Portfolio fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioPublicResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioPublicResponse" } } } }, @@ -3195,9 +2856,7 @@ "description": "The subdomain is empty or reserved once normalised.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -3210,14 +2869,8 @@ "description": "No live portfolio exists at this subdomain.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Portfolio not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Portfolio not found" } } } } @@ -3230,21 +2883,9 @@ "operationId": "recordPortfolioView", "summary": "Record Portfolio View", "description": "Records an analytics view for a published portfolio. Responds `202 Accepted` with\n`data: null` โ€” the count is not persisted synchronously.\n\nViews are buffered in Redis, deduplicated per viewer IP for 30 minutes, and flushed to\nPostgres by a scheduled job. A repeat view inside that window is accepted and silently\ndropped, so a `202` does not guarantee the count increased.\n\nNo user session is required, but the request must carry an API key or originate from a\nwhitelisted first-party origin; otherwise it is rejected with `401`.\n", - "security": [ - { - "ApiKeyAuth": [] - }, - {} - ], + "security": [{ "ApiKeyAuth": [] }, {}], "parameters": [ - { - "name": "subdomain", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "subdomain", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": false, @@ -3267,14 +2908,8 @@ "description": "View accepted", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/NullDataResponse" - }, - "example": { - "success": true, - "message": "Success", - "data": null - } + "schema": { "$ref": "#/components/schemas/NullDataResponse" }, + "example": { "success": true, "message": "Success", "data": null } } } }, @@ -3282,14 +2917,8 @@ "description": "No live portfolio exists at this subdomain.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Portfolio not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Portfolio not found" } } } } @@ -3300,14 +2929,7 @@ "get": { "tags": ["Portfolios"], "operationId": "getMyPortfolio", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get My Portfolio", "description": "Returns the caller's current portfolio draft document (if any), publication status, and\nbilling summary in one call. Session-authenticated only โ€” this route does not accept an API\nkey.\n", "responses": { @@ -3315,15 +2937,11 @@ "description": "Portfolio state fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioMeResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioMeResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -3331,23 +2949,14 @@ "put": { "tags": ["Portfolios"], "operationId": "savePortfolioDraft", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Save Portfolio Draft", "description": "Creates or updates the caller's single PORTFOLIO document with a new content snapshot.\nUses optimistic concurrency via `revision` once a draft already exists. Session-authenticated\nonly.\n\nThe response is a trimmed projection of the document, not a full `Document` record.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioSaveDraftRequest" - } + "schema": { "$ref": "#/components/schemas/PortfolioSaveDraftRequest" } } } }, @@ -3356,35 +2965,23 @@ "description": "Draft saved successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioDraftResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioDraftResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Portfolio draft not found (when documentId was provided but doesn't belong to the caller)", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "409": { "description": "Draft was changed in another session, or the subdomain conflicts with an existing document slug", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 409, @@ -3400,42 +2997,22 @@ "get": { "tags": ["Portfolios"], "operationId": "checkPortfolioSubdomainAvailability", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Check Portfolio Subdomain Availability", "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Availability checked successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioSubdomainAvailabilityResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioSubdomainAvailabilityResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -3443,47 +3020,27 @@ "get": { "tags": ["Portfolios"], "operationId": "previewPortfolio", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Preview Portfolio Draft", "description": "Returns the caller's own portfolio draft for rendering a preview, without publishing it.\nThe response is a trimmed projection of the document, not a full `Document` record โ€” it has\nno `slug`. Session-authenticated only.\n", "parameters": [ - { - "name": "documentId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "documentId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Draft fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioPreviewResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioPreviewResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Portfolio draft not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -3499,23 +3056,14 @@ "post": { "tags": ["Portfolios"], "operationId": "publishPortfolio", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Publish Portfolio", "description": "Publishes the given draft revision to its chosen subdomain, making it publicly reachable.\nPremium templates (`nimbus`, `cipher`) require an active Creator Pro subscription.\n\n**Current-state caveat:** in production, publishing is gated behind a hardcoded\n`ADMIN_EMAIL` check independent of plan tier (`isAdminUser`) โ€” regular users can save drafts,\npreview, and manage everything except actually flipping a portfolio live. This is a\ntemporary rollout restriction, not a permanent product limitation. Only administrators can\nexercise this endpoint end-to-end in production today; it is unrestricted in development.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioPublishRequest" - } + "schema": { "$ref": "#/components/schemas/PortfolioPublishRequest" } } } }, @@ -3524,9 +3072,7 @@ "description": "Portfolio published successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioPublishResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioPublishResponse" } } } }, @@ -3534,9 +3080,7 @@ "description": "Validation error, incomplete portfolio content, or an unusable image reference", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "incompleteContent": { "summary": "Draft does not satisfy the stricter publish schema", @@ -3558,16 +3102,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "Production publishing restriction, or premium template requires a subscription", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "productionGate": { "summary": "Non-admin in production", @@ -3592,20 +3132,14 @@ "404": { "description": "Portfolio draft not found", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "409": { "description": "Stale revision, or subdomain already in use", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 409, @@ -3621,14 +3155,7 @@ "post": { "tags": ["Portfolios"], "operationId": "unpublishPortfolio", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Unpublish Portfolio", "description": "Takes a published portfolio offline โ€” status becomes `SUSPENDED` with reason\n`user_unpublished`, and the underlying document reverts to `PRIVATE` visibility. The document\nitself is not deleted, so the portfolio can be republished later.\n\nIdempotent: if the caller has never published, the call still succeeds and returns\n`data: null`. Session-authenticated only.\n", "responses": { @@ -3636,15 +3163,11 @@ "description": "Portfolio unpublished successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioUnpublishResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioUnpublishResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -3652,14 +3175,7 @@ "get": { "tags": ["Portfolios"], "operationId": "getPortfolioAnalytics", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Portfolio Analytics", "description": "Views for the last 30 days plus top referrer hosts. Returns all-zero data if the caller has never published.", "responses": { @@ -3667,15 +3183,11 @@ "description": "Analytics fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioAnalyticsResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioAnalyticsResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -3683,28 +3195,15 @@ "post": { "tags": ["Portfolio Assets"], "operationId": "createPortfolioAssetUploadUrl", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Create Portfolio Asset Upload URL", "description": "Issues a presigned Cloudflare R2 PUT URL (10-minute expiry) for a JPG, PNG, or WebP image up\nto 5MB. Upload the file directly to `uploadUrl` with a matching Content-Type/Content-Length,\nthen call `POST /portfolio-assets/complete` to verify and activate it. Session-authenticated\nonly.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioAssetUploadRequest" - }, - "example": { - "kind": "AVATAR", - "mimeType": "image/png", - "sizeBytes": 204800 - } + "schema": { "$ref": "#/components/schemas/PortfolioAssetUploadRequest" }, + "example": { "kind": "AVATAR", "mimeType": "image/png", "sizeBytes": 204800 } } } }, @@ -3713,9 +3212,7 @@ "description": "Upload URL created successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioAssetUploadResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioAssetUploadResponse" } } } }, @@ -3723,9 +3220,7 @@ "description": "Unsupported image type, or size out of range", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -3734,17 +3229,11 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "503": { "description": "Media uploads are not configured (R2 credentials missing)", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } @@ -3754,23 +3243,14 @@ "post": { "tags": ["Portfolio Assets"], "operationId": "completePortfolioAssetUpload", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Complete Portfolio Asset Upload", "description": "Verifies an uploaded asset with a server-side HEAD request against R2 (confirming size,\ncontent type, and optional checksum match what was declared) and marks it `READY` so it can\nbe referenced from a portfolio. Session-authenticated only.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioAssetCompleteRequest" - } + "schema": { "$ref": "#/components/schemas/PortfolioAssetCompleteRequest" } } } }, @@ -3779,32 +3259,22 @@ "description": "Asset completed successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioAssetCompleteResponse" - } + "schema": { "$ref": "#/components/schemas/PortfolioAssetCompleteResponse" } } } }, "400": { "description": "Uploaded file does not match the declared size/type/checksum", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Asset not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -3816,11 +3286,7 @@ "503": { "description": "Media delivery is not configured (public base URL missing)", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } @@ -3830,52 +3296,28 @@ "delete": { "tags": ["Portfolio Assets"], "operationId": "deletePortfolioAsset", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Delete Portfolio Asset", "description": "Deletes the R2 object and its metadata row. `data` is `null` on success.\nSession-authenticated only.\n\nThis does not rewrite any portfolio that still references the asset โ€” remove the reference\nand save the draft first, or publishing will fail with `400`.\n", "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Asset deleted successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/NullDataResponse" - }, - "example": { - "success": true, - "message": "Success", - "data": null - } + "schema": { "$ref": "#/components/schemas/NullDataResponse" }, + "example": { "success": true, "message": "Success", "data": null } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Asset not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -3891,14 +3333,7 @@ "get": { "tags": ["Billing"], "operationId": "getBillingSummary", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Billing Summary", "description": "Returns the caller's plan, subscription status, entitlements, AI credit wallet, and public\ncatalog in one call. Entitlements (e.g. portfolio publishing) and the AI credit wallet are\ntwo independent systems โ€” a plan can grant one, both, or neither depending on `productKey`.\nSession-authenticated only.\n", "responses": { @@ -3906,23 +3341,15 @@ "description": "Billing summary fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/BillingSummaryResponse" - } + "schema": { "$ref": "#/components/schemas/BillingSummaryResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "User not found", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } @@ -3932,14 +3359,7 @@ "get": { "tags": ["Billing"], "operationId": "getBillingHistory", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Billing History", "description": "Lists the caller's processed billing webhook events (most recent 50). Session-authenticated only.", "responses": { @@ -3947,15 +3367,11 @@ "description": "History fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/BillingHistoryResponse" - } + "schema": { "$ref": "#/components/schemas/BillingHistoryResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -3963,14 +3379,7 @@ "get": { "tags": ["Billing"], "operationId": "getCreditWallet", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get AI Credit Wallet", "description": "Returns the caller's current AI credit balance, reserved amount, and next expiring grant. Session-authenticated only.", "responses": { @@ -3978,15 +3387,11 @@ "description": "Wallet fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CreditWalletResponse" - } + "schema": { "$ref": "#/components/schemas/CreditWalletResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -3994,14 +3399,7 @@ "get": { "tags": ["Billing"], "operationId": "getCreditHistory", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get AI Credit Transaction History", "description": "Lists recent AI credit reservations/commits/releases for the caller's wallet. Session-authenticated only.", "responses": { @@ -4009,15 +3407,11 @@ "description": "History fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CreditHistoryResponse" - } + "schema": { "$ref": "#/components/schemas/CreditHistoryResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -4025,27 +3419,15 @@ "post": { "tags": ["Billing"], "operationId": "createBillingCheckout", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Create Subscription Checkout Session", "description": "Creates a hosted Dodo Payments checkout session for a subscription product. `productKey` and\n`interval` are validated against a fixed whitelist (the product catalog) before being handed\noff to Dodo โ€” arbitrary product/interval combinations are rejected with `400`. A Redis lock\nprevents starting a second checkout while one is already in flight. Session-authenticated\nonly; blocked for non-admins in production during the current rollout phase.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckoutRequest" - }, - "example": { - "productKey": "portfolio_pro", - "interval": "monthly" - } + "schema": { "$ref": "#/components/schemas/CheckoutRequest" }, + "example": { "productKey": "portfolio_pro", "interval": "monthly" } } } }, @@ -4053,20 +3435,14 @@ "200": { "description": "Checkout session created successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckoutResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutResponse" } } } }, "400": { "description": "Invalid product/interval combination", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -4075,16 +3451,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "Payments disabled in production for non-admins", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 403, @@ -4096,21 +3468,13 @@ "409": { "description": "A checkout is already active, or the caller already has an active subscription", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "503": { "description": "The selected billing product is not configured", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } @@ -4120,14 +3484,7 @@ "post": { "tags": ["Billing"], "operationId": "cancelBillingCheckout", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Cancel In-Flight Checkout", "description": "Releases the Redis lock created by `POST /billing/checkout`, allowing a new checkout to be started. Session-authenticated only.", "responses": { @@ -4135,15 +3492,11 @@ "description": "Checkout cancelled successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelCheckoutResponse" - } + "schema": { "$ref": "#/components/schemas/CancelCheckoutResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -4151,14 +3504,7 @@ "post": { "tags": ["Billing"], "operationId": "createBillingPortal", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Create Billing Portal Session", "description": "Creates a Dodo Payments customer portal URL for managing an existing subscription. Session-authenticated only; blocked for non-admins in production during the current rollout phase.", "responses": { @@ -4166,32 +3512,22 @@ "description": "Portal session created successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/BillingPortalResponse" - } + "schema": { "$ref": "#/components/schemas/BillingPortalResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "Payments disabled in production for non-admins", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "409": { "description": "No billing account exists yet", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 409, @@ -4207,26 +3543,15 @@ "post": { "tags": ["Billing"], "operationId": "createCreditPackCheckout", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Create Extra Credit Pack Checkout", "description": "Creates a one-time Dodo Payments checkout session for an extra AI credit pack, on top of any\nsubscription. Session-authenticated only; blocked for non-admins in production during the\ncurrent rollout phase.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CreditPackCheckoutRequest" - }, - "example": { - "packKey": "credit_pack_250" - } + "schema": { "$ref": "#/components/schemas/CreditPackCheckoutRequest" }, + "example": { "packKey": "credit_pack_250" } } } }, @@ -4235,36 +3560,22 @@ "description": "Checkout session created successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CreditPackCheckoutResponse" - } + "schema": { "$ref": "#/components/schemas/CreditPackCheckoutResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "Payments disabled in production for non-admins", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "503": { "description": "Extra credit checkout is not configured", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } @@ -4274,31 +3585,15 @@ "post": { "tags": ["Shares"], "operationId": "createShareLink", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Create Share Link", "description": "Creates (or replaces, if one already exists for this document) a public share link for a\ndocument. Optionally password-protected โ€” passwords are scrypt-hashed and never returned in\nany response โ€” and optionally set to expire.\n\nThe caller **must have claimed a username first**, since it forms the public URL\n(`/{username}/{slug}`); without one the request fails with `400`. Requires the\n`resume:write` scope when called with an API key.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ShareLinkCreateRequest" - }, - "example": { - "documentId": "doc_123", - "snapshot": {}, - "noExpiry": true - } + "schema": { "$ref": "#/components/schemas/ShareLinkCreateRequest" }, + "example": { "documentId": "doc_123", "snapshot": {}, "noExpiry": true } } } }, @@ -4306,20 +3601,14 @@ "201": { "description": "Share link created successfully", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShareLinkResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ShareLinkResponse" } } } }, "400": { "description": "Validation error, or the caller has not claimed a username yet.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "noUsername": { "summary": "Username not set", @@ -4347,21 +3636,13 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Document not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Document not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Document not found" } } } } @@ -4372,17 +3653,7 @@ "get": { "tags": ["Shares"], "operationId": "listSharedDocumentIds", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "List Shared Document IDs", "description": "Given a comma-separated list of document IDs, returns which of them currently have an active (non-expired) share link. Useful for badging a document list in the UI.", "parameters": [ @@ -4390,9 +3661,7 @@ "name": "ids", "in": "query", "required": false, - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "description": "Comma-separated document IDs." } ], @@ -4401,15 +3670,11 @@ "description": "Shared document ids fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedDocumentIdsResponse" - } + "schema": { "$ref": "#/components/schemas/SharedDocumentIdsResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -4417,48 +3682,22 @@ "get": { "tags": ["Shares"], "operationId": "listShareLinksForDocument", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "List Share Links For Document", "description": "Returns the (at most one) share link for a document, offset-paginated for shape consistency with other list endpoints.", "parameters": [ - { - "name": "documentId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, + { "name": "documentId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "limit", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 20 - } + "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 } }, { "name": "offset", "in": "query", "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0 - } + "schema": { "type": "integer", "minimum": 0, "default": 0 } } ], "responses": { @@ -4466,15 +3705,11 @@ "description": "Share links fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ShareLinkListResponse" - } + "schema": { "$ref": "#/components/schemas/ShareLinkListResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" } } } }, @@ -4482,45 +3717,19 @@ "delete": { "tags": ["Shares"], "operationId": "revokeShareLink", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "ApiKeyAuth": [] }, { "LocalSession": [] }, { "ProdSession": [] }], "summary": "Revoke Share Link", "description": "Deletes the share link, immediately breaking its public URL. `data` is `null` on success.\nRequires the `resume:write` scope when called with an API key.\n", "parameters": [ - { - "name": "documentId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "shareLinkId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "documentId", "in": "path", "required": true, "schema": { "type": "string" } }, + { "name": "shareLinkId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Share link revoked successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/NullDataResponse" - }, + "schema": { "$ref": "#/components/schemas/NullDataResponse" }, "example": { "success": true, "message": "Share link revoked successfully", @@ -4529,16 +3738,12 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Share link not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 404, @@ -4556,38 +3761,17 @@ "operationId": "getPublicShareLink", "summary": "Get Public Share Link", "description": "Returns the shared document's title and snapshot, unless the link is password-protected โ€” in\nwhich case only `passwordRequired: true` is returned and the caller must call\n`POST /shares/public/{username}/{slug}/verify` with the password.\n\nNo user session is required, but the request is still gated: it must carry an API key, or\noriginate from a whitelisted first-party origin. A bare request from an unrecognised origin\nis rejected with `401`.\n", - "security": [ - { - "ApiKeyAuth": [] - }, - {} - ], + "security": [{ "ApiKeyAuth": [] }, {}], "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "username", "in": "path", "required": true, "schema": { "type": "string" } }, + { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Shared document fetched successfully (or password required)", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicShareResponse" - } + "schema": { "$ref": "#/components/schemas/PublicShareResponse" } } } }, @@ -4595,14 +3779,8 @@ "description": "Link not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Link not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Link not found" } } } }, @@ -4610,14 +3788,8 @@ "description": "Link expired", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 410, - "message": "Link expired" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 410, "message": "Link expired" } } } } @@ -4630,37 +3802,16 @@ "operationId": "verifyPublicShareLinkPassword", "summary": "Verify Public Share Link Password", "description": "Verifies a password-protected share link's password using a timing-safe comparison against\nthe stored scrypt hash, and returns the full snapshot on success.\n\nNo user session is required, but the request must carry an API key or originate from a\nwhitelisted first-party origin, same as the read endpoint.\n\n**Rate limited to 3 attempts per 5 minutes per IP** โ€” considerably tighter than the rest of\nthe API, to make these links impractical to brute-force.\n", - "security": [ - { - "ApiKeyAuth": [] - }, - {} - ], + "security": [{ "ApiKeyAuth": [] }, {}], "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } + { "name": "username", "in": "path", "required": true, "schema": { "type": "string" } }, + { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifySharePasswordRequest" - } + "schema": { "$ref": "#/components/schemas/VerifySharePasswordRequest" } } } }, @@ -4669,27 +3820,17 @@ "description": "Shared document unlocked successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicShareResponse" - } + "schema": { "$ref": "#/components/schemas/PublicShareResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, "401": { "description": "Invalid password", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 401, - "message": "Invalid password" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 401, "message": "Invalid password" } } } }, @@ -4697,14 +3838,8 @@ "description": "Link not found", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 404, - "message": "Link not found" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 404, "message": "Link not found" } } } }, @@ -4712,14 +3847,8 @@ "description": "Link expired", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 410, - "message": "Link expired" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 410, "message": "Link expired" } } } }, @@ -4727,9 +3856,7 @@ "description": "Too many password attempts โ€” 3 per 5 minutes per IP.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 429, @@ -4751,12 +3878,8 @@ "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateClickRequest" - }, - "example": { - "code": "jane-doe1" - } + "schema": { "$ref": "#/components/schemas/AffiliateClickRequest" }, + "example": { "code": "jane-doe1" } } } }, @@ -4765,28 +3888,18 @@ "description": "Click tracked successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateClickResponse" - } + "schema": { "$ref": "#/components/schemas/AffiliateClickResponse" } } } }, - "400": { - "$ref": "#/components/responses/ValidationError" - }, + "400": { "$ref": "#/components/responses/ValidationError" }, "404": { "description": "Affiliate link not found", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -4801,11 +3914,7 @@ "name": "period", "in": "query", "required": false, - "schema": { - "type": "string", - "enum": ["monthly", "all_time"], - "default": "monthly" - } + "schema": { "type": "string", "enum": ["monthly", "all_time"], "default": "monthly" } } ], "responses": { @@ -4813,15 +3922,11 @@ "description": "Leaderboard fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateLeaderboardResponse" - } + "schema": { "$ref": "#/components/schemas/AffiliateLeaderboardResponse" } } } }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -4829,14 +3934,7 @@ "get": { "tags": ["Affiliates"], "operationId": "getMyAffiliateDashboard", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get My Affiliate Dashboard", "description": "Returns the caller's affiliate status, tier, wallet, clicks, referrals, commissions, and withdrawal history. Session-authenticated only.", "responses": { @@ -4844,18 +3942,12 @@ "description": "Dashboard fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateDashboardResponse" - } + "schema": { "$ref": "#/components/schemas/AffiliateDashboardResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -4863,14 +3955,7 @@ "post": { "tags": ["Affiliates"], "operationId": "enrollAsAffiliate", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Enroll As Affiliate", "description": "Enrolls the caller in the affiliate program, generating a unique referral code. Idempotent โ€” calling this again while already enrolled just returns the current dashboard. Session-authenticated only.", "responses": { @@ -4878,18 +3963,12 @@ "description": "Enrolled successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateDashboardResponse" - } + "schema": { "$ref": "#/components/schemas/AffiliateDashboardResponse" } } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -4897,23 +3976,14 @@ "post": { "tags": ["Affiliates"], "operationId": "applyAffiliateReferral", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Apply Affiliate Referral Code", "description": "Attaches another affiliate's referral code to the caller's account. Self-referrals are rejected, and each account may only apply one referral code, ever. Session-authenticated only.", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateReferralRequest" - } + "schema": { "$ref": "#/components/schemas/AffiliateReferralRequest" } } } }, @@ -4922,9 +3992,7 @@ "description": "Referral applied successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateReferralResponse" - } + "schema": { "$ref": "#/components/schemas/AffiliateReferralResponse" } } } }, @@ -4932,9 +4000,7 @@ "description": "Validation error, or attempted self-referral", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -4943,32 +4009,20 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Affiliate code not found", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "409": { "description": "This account already has a referral", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -4976,26 +4030,15 @@ "post": { "tags": ["Affiliates"], "operationId": "requestAffiliateWithdrawal", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Request Affiliate Withdrawal", "description": "Requests a payout of available affiliate commission balance (minimum $25 / 2,500 cents).\nPayouts are reviewed and marked paid manually by an admin out-of-band (bank transfer, PayPal,\netc) โ€” Dodo Payments only exposes a read-only settlements API, not an outbound transfer API,\nso this cannot be automated end-to-end yet. Session-authenticated only.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateWithdrawalRequest" - }, - "example": { - "amountCents": 5000 - } + "schema": { "$ref": "#/components/schemas/AffiliateWithdrawalRequest" }, + "example": { "amountCents": 5000 } } } }, @@ -5004,9 +4047,7 @@ "description": "Withdrawal requested successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AffiliateWithdrawalResponse" - } + "schema": { "$ref": "#/components/schemas/AffiliateWithdrawalResponse" } } } }, @@ -5014,9 +4055,7 @@ "description": "Available balance is too low", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 400, @@ -5025,22 +4064,14 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "409": { "description": "An active withdrawal request already exists", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -5048,14 +4079,7 @@ "get": { "tags": ["Ambassador"], "operationId": "getAmbassadorStatus", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Get Ambassador Status", "description": "Returns the caller's current ambassador application status. This is intentionally a small\nsurface today โ€” application intake and status are the only things live. There is no\npoints/rewards/redemption API yet; that system has not been built.\n\nGated behind a feature flag (`config.growth.ambassadorProgramEnabled`) that defaults off in\nproduction until explicitly enabled โ€” every route in this group returns `503` while the flag\nis off. Session-authenticated only.\n", "responses": { @@ -5063,9 +4087,7 @@ "description": "Status fetched successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AmbassadorStatusResponse" - }, + "schema": { "$ref": "#/components/schemas/AmbassadorStatusResponse" }, "example": { "success": true, "message": "Success", @@ -5079,22 +4101,14 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, + "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "User not found", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } }, @@ -5102,23 +4116,14 @@ "post": { "tags": ["Ambassador"], "operationId": "applyForAmbassador", - "security": [ - { - "LocalSession": [] - }, - { - "ProdSession": [] - } - ], + "security": [{ "LocalSession": [] }, { "ProdSession": [] }], "summary": "Apply For Campus Ambassador Program", "description": "Submits (or resubmits, once previously rejected) a campus ambassador application. Rejects a\nduplicate application from a user who already has one pending review, and rejects existing\nambassadors from re-applying. `graduationYear` is validated as a real 4-digit year on both the\nclient and the server. Session-authenticated only.\n", "requestBody": { "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AmbassadorApplicationRequest" - }, + "schema": { "$ref": "#/components/schemas/AmbassadorApplicationRequest" }, "example": { "collegeName": "State University", "graduationYear": "2027", @@ -5134,9 +4139,7 @@ "description": "Application submitted successfully", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AmbassadorApplicationResponse" - }, + "schema": { "$ref": "#/components/schemas/AmbassadorApplicationResponse" }, "example": { "success": true, "message": "Success", @@ -5154,9 +4157,7 @@ "description": "Already an ambassador, application already pending, or validation error", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "alreadyAmbassador": { "summary": "Already approved", @@ -5178,12 +4179,8 @@ } } }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "503": { - "$ref": "#/components/responses/FeatureDisabledError" - } + "401": { "$ref": "#/components/responses/UnauthorizedError" }, + "503": { "$ref": "#/components/responses/FeatureDisabledError" } } } } @@ -5214,28 +4211,15 @@ "type": "object", "required": ["success", "message", "statusCode"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "statusCode": { - "type": "integer" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "statusCode": { "type": "integer" }, "details": { "type": "array", "nullable": true, "items": { "type": "object", - "properties": { - "path": { - "type": "string" - }, - "message": { - "type": "string" - } - } + "properties": { "path": { "type": "string" }, "message": { "type": "string" } } } } } @@ -5258,36 +4242,20 @@ "type": "integer", "description": "Total number of matching records, ignoring the current window." }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, + "limit": { "type": "integer" }, + "offset": { "type": "integer" }, "page": { "type": "integer", "description": "1-based page index for the current window." }, - "pageSize": { - "type": "integer", - "description": "Always equal to `limit`." - }, - "totalPages": { - "type": "integer", - "description": "At least 1, even when `total` is 0." - }, - "hasMore": { - "type": "boolean" - }, + "pageSize": { "type": "integer", "description": "Always equal to `limit`." }, + "totalPages": { "type": "integer", "description": "At least 1, even when `total` is 0." }, + "hasMore": { "type": "boolean" }, "pagination": { "type": "object", "required": ["mode", "nextOffset", "nextCursor"], "properties": { - "mode": { - "type": "string", - "const": "offset", - "enum": ["offset"] - }, + "mode": { "type": "string", "const": "offset", "enum": ["offset"] }, "nextOffset": { "type": "integer", "nullable": true, @@ -5307,30 +4275,17 @@ "description": "Liveness payload. `GET /api/v1/health` deliberately touches no external dependency, so uptime\nchecks do not wake database compute โ€” it reports only that the Node.js process is responding.\n", "required": ["status", "timestamp"], "properties": { - "status": { - "type": "string", - "const": "ok", - "example": "ok" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } + "status": { "type": "string", "const": "ok", "example": "ok" }, + "timestamp": { "type": "string", "format": "date-time" } } }, "HealthLivenessResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/HealthLivenessData" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/HealthLivenessData" } } }, "HealthReadyData": { @@ -5338,40 +4293,19 @@ "description": "Readiness payload. Only `GET /api/v1/health/ready` verifies PostgreSQL and Redis connectivity.", "required": ["status", "database", "redis", "timestamp"], "properties": { - "status": { - "type": "string", - "const": "ok", - "example": "ok" - }, - "database": { - "type": "string", - "const": "connected", - "example": "connected" - }, - "redis": { - "type": "string", - "const": "connected", - "example": "connected" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } + "status": { "type": "string", "const": "ok", "example": "ok" }, + "database": { "type": "string", "const": "connected", "example": "connected" }, + "redis": { "type": "string", "const": "connected", "example": "connected" }, + "timestamp": { "type": "string", "format": "date-time" } } }, "HealthReadyResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/HealthReadyData" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/HealthReadyData" } } }, "UserObject": { @@ -5386,46 +4320,25 @@ "_count" ], "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string", - "nullable": true - }, + "id": { "type": "string" }, + "name": { "type": "string", "nullable": true }, "username": { "type": "string", "nullable": true, "description": "Null until the user claims one. Immutable once set, and required before any share link can\nbe created.\n" }, - "email": { - "type": "string" - }, - "emailVerified": { - "type": "boolean" - }, - "autoSyncEnabled": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, + "email": { "type": "string" }, + "emailVerified": { "type": "boolean" }, + "autoSyncEnabled": { "type": "boolean" }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, "_count": { "type": "object", "description": "Relational counts for the user's own records.", "required": ["apiKeys", "shareLinks", "resumes"], "properties": { - "apiKeys": { - "type": "integer" - }, - "shareLinks": { - "type": "integer" - }, + "apiKeys": { "type": "integer" }, + "shareLinks": { "type": "integer" }, "resumes": { "type": "integer", "description": "Counts every Document the user owns โ€” every `type`, and soft-deleted ones included.\nIt is not a count of RESUME-type documents. Use `GET /documents` for an accurate,\nfilterable list.\n" @@ -5438,17 +4351,36 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/UserObject" } + } + }, + "UsernameAvailability": { + "type": "object", + "required": ["available", "normalizedUsername"], + "properties": { + "available": { "type": "boolean" }, + "normalizedUsername": { + "type": "string", + "description": "The value that would actually be stored, after server-side normalisation." }, - "data": { - "$ref": "#/components/schemas/UserObject" + "reason": { + "type": "string", + "enum": ["too_short", "too_long", "invalid_characters", "reserved", "taken"], + "description": "Present only when `available` is `false`." } } }, + "UsernameAvailabilityResponse": { + "type": "object", + "required": ["success", "message", "data"], + "properties": { + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/UsernameAvailability" } + } + }, "MasterProfile": { "type": "object", "required": ["id", "userId", "content", "createdAt", "updatedAt"], @@ -5457,45 +4389,21 @@ "type": "string", "description": "Empty string when the user has never saved a master profile yet." }, - "userId": { - "type": "string" - }, - "content": { - "$ref": "#/components/schemas/MasterProfileContent" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "userId": { "type": "string" }, + "content": { "$ref": "#/components/schemas/MasterProfileContent" }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "UserSummary": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "emailVerified": { - "type": "boolean" - }, - "autoSyncEnabled": { - "type": "boolean" - }, + "id": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "email": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" }, + "emailVerified": { "type": "boolean" }, + "autoSyncEnabled": { "type": "boolean" }, "shareResumeCount": { "type": "integer", "description": "Number of ShareLink records owned by the user." @@ -5505,9 +4413,7 @@ "MasterProfileLink": { "type": "object", "properties": { - "id": { - "type": "string" - }, + "id": { "type": "string" }, "type": { "type": "string", "enum": [ @@ -5522,12 +4428,8 @@ "custom" ] }, - "label": { - "type": "string" - }, - "url": { - "type": "string" - } + "label": { "type": "string" }, + "url": { "type": "string" } } }, "MasterProfileBasics": { @@ -5545,18 +4447,9 @@ "linkLocation" ], "properties": { - "fullName": { - "type": "string", - "maxLength": 120 - }, - "role": { - "type": "string", - "maxLength": 120 - }, - "headline": { - "type": "string", - "maxLength": 250 - }, + "fullName": { "type": "string", "maxLength": 120 }, + "role": { "type": "string", "maxLength": 120 }, + "headline": { "type": "string", "maxLength": 250 }, "email": { "type": "string", "format": "email", @@ -5567,155 +4460,66 @@ "maxLength": 24, "description": "Must contain exactly 10 digits once non-digit characters are ignored, so `\"9876543210\"` and\n`\"(987) 654-3210\"` both pass. The value is stored as sent โ€” it is not reformatted โ€” and an\nempty string is rejected.\n" }, - "location": { - "type": "string", - "maxLength": 120 - }, - "linkEmail": { - "type": "boolean" - }, - "linkPhone": { - "type": "boolean" - }, - "linkLocation": { - "type": "boolean" - } + "location": { "type": "string", "maxLength": 120 }, + "linkEmail": { "type": "boolean" }, + "linkPhone": { "type": "boolean" }, + "linkLocation": { "type": "boolean" } } }, "MasterProfileExperienceItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "company": { - "type": "string" - }, - "role": { - "type": "string" - }, - "location": { - "type": "string" - }, - "startDate": { - "type": "string", - "description": "YYYY-MM" - }, - "endDate": { - "type": "string", - "description": "YYYY-MM" - }, - "current": { - "type": "boolean" - }, - "summary": { - "type": "string" - }, - "highlights": { - "type": "array", - "items": { - "type": "string" - } - } + "id": { "type": "string" }, + "company": { "type": "string" }, + "role": { "type": "string" }, + "location": { "type": "string" }, + "startDate": { "type": "string", "description": "YYYY-MM" }, + "endDate": { "type": "string", "description": "YYYY-MM" }, + "current": { "type": "boolean" }, + "summary": { "type": "string" }, + "highlights": { "type": "array", "items": { "type": "string" } } } }, "MasterProfileEducationItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "school": { - "type": "string" - }, - "degree": { - "type": "string" - }, - "field": { - "type": "string" - }, - "startDate": { - "type": "string", - "description": "YYYY" - }, - "endDate": { - "type": "string", - "description": "YYYY" - }, - "current": { - "type": "boolean" - }, - "summary": { - "type": "string" - } + "id": { "type": "string" }, + "school": { "type": "string" }, + "degree": { "type": "string" }, + "field": { "type": "string" }, + "startDate": { "type": "string", "description": "YYYY" }, + "endDate": { "type": "string", "description": "YYYY" }, + "current": { "type": "boolean" }, + "summary": { "type": "string" } } }, "MasterProfileProjectItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "role": { - "type": "string" - }, - "link": { - "type": "string" - }, - "linkLabel": { - "type": "string", - "default": "Link" - }, - "showLinkAsText": { - "type": "boolean", - "default": true - }, - "summary": { - "type": "string" - }, - "highlights": { - "type": "array", - "items": { - "type": "string" - } - }, - "skills": { - "type": "array", - "items": { - "type": "string" - } - } + "id": { "type": "string" }, + "name": { "type": "string" }, + "role": { "type": "string" }, + "link": { "type": "string" }, + "linkLabel": { "type": "string", "default": "Link" }, + "showLinkAsText": { "type": "boolean", "default": true }, + "summary": { "type": "string" }, + "highlights": { "type": "array", "items": { "type": "string" } }, + "skills": { "type": "array", "items": { "type": "string" } } } }, "MasterProfileSkillItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "keywords": { - "type": "array", - "items": { - "type": "string" - } - } + "id": { "type": "string" }, + "name": { "type": "string" }, + "keywords": { "type": "array", "items": { "type": "string" } } } }, "MasterProfileLanguageItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "language": { - "type": "string" - }, + "id": { "type": "string" }, + "language": { "type": "string" }, "fluency": { "type": "string", "enum": ["elementary", "limited", "professional", "fluent", "native"] @@ -5725,208 +4529,97 @@ "MasterProfileInterestItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "keywords": { - "type": "array", - "items": { - "type": "string" - } - } + "id": { "type": "string" }, + "name": { "type": "string" }, + "keywords": { "type": "array", "items": { "type": "string" } } } }, "MasterProfileAwardItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "awarder": { - "type": "string" - }, - "date": { - "type": "string", - "description": "YYYY-MM" - }, - "website": { - "type": "string" - }, - "description": { - "type": "string" - }, - "showLink": { - "type": "boolean" - } + "id": { "type": "string" }, + "title": { "type": "string" }, + "awarder": { "type": "string" }, + "date": { "type": "string", "description": "YYYY-MM" }, + "website": { "type": "string" }, + "description": { "type": "string" }, + "showLink": { "type": "boolean" } } }, "MasterProfileCertificateItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "issuer": { - "type": "string" - }, - "date": { - "type": "string", - "description": "YYYY-MM" - }, - "website": { - "type": "string" - }, - "description": { - "type": "string" - }, - "showLink": { - "type": "boolean" - } + "id": { "type": "string" }, + "title": { "type": "string" }, + "issuer": { "type": "string" }, + "date": { "type": "string", "description": "YYYY-MM" }, + "website": { "type": "string" }, + "description": { "type": "string" }, + "showLink": { "type": "boolean" } } }, "MasterProfilePublicationItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "publisher": { - "type": "string" - }, - "date": { - "type": "string", - "description": "YYYY-MM" - }, - "website": { - "type": "string" - }, - "description": { - "type": "string" - }, - "showLink": { - "type": "boolean" - } + "id": { "type": "string" }, + "title": { "type": "string" }, + "publisher": { "type": "string" }, + "date": { "type": "string", "description": "YYYY-MM" }, + "website": { "type": "string" }, + "description": { "type": "string" }, + "showLink": { "type": "boolean" } } }, "MasterProfileVolunteerItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "organization": { - "type": "string" - }, - "role": { - "type": "string" - }, - "startDate": { - "type": "string" - }, - "endDate": { - "type": "string" - }, - "current": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "summary": { - "type": "string" - } + "id": { "type": "string" }, + "organization": { "type": "string" }, + "role": { "type": "string" }, + "startDate": { "type": "string" }, + "endDate": { "type": "string" }, + "current": { "type": "boolean" }, + "location": { "type": "string" }, + "summary": { "type": "string" } } }, "MasterProfileReferenceItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "organization": { - "type": "string" - }, - "email": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "relationship": { - "type": "string" - } + "id": { "type": "string" }, + "name": { "type": "string" }, + "title": { "type": "string" }, + "organization": { "type": "string" }, + "email": { "type": "string" }, + "phone": { "type": "string" }, + "relationship": { "type": "string" } } }, "MasterProfileAchievementItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - } + "id": { "type": "string" }, + "title": { "type": "string" }, + "description": { "type": "string" } } }, "MasterProfileAdditionalItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "issuer": { - "type": "string" - }, - "date": { - "type": "string" - }, - "link": { - "type": "string" - }, - "referenceId": { - "type": "string" - }, - "description": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "string" - } - } + "id": { "type": "string" }, + "name": { "type": "string" }, + "issuer": { "type": "string" }, + "date": { "type": "string" }, + "link": { "type": "string" }, + "referenceId": { "type": "string" }, + "description": { "type": "string" }, + "details": { "type": "array", "items": { "type": "string" } } } }, "MasterProfileCustomSection": { "type": "object", "properties": { - "id": { - "type": "string" - }, + "id": { "type": "string" }, "kind": { "type": "string", "enum": [ @@ -5941,18 +4634,12 @@ "custom" ] }, - "title": { - "type": "string" - }, + "title": { "type": "string" }, "items": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileAdditionalItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileAdditionalItem" } }, - "editableTitle": { - "type": "boolean" - } + "editableTitle": { "type": "boolean" } } }, "MasterProfileSectionItem": { @@ -5979,56 +4666,26 @@ "custom" ] }, - "label": { - "type": "string" - }, - "visible": { - "type": "boolean" - }, - "order": { - "type": "integer" - } + "label": { "type": "string" }, + "visible": { "type": "boolean" }, + "order": { "type": "integer" } } }, "MasterProfileCustomization": { "type": "object", "properties": { - "accentColor": { - "type": "string" - }, - "textColor": { - "type": "string" - }, - "mutedTextColor": { - "type": "string" - }, - "pageBackgroundColor": { - "type": "string" - }, - "sectionBackgroundColor": { - "type": "string" - }, - "borderColor": { - "type": "string" - }, - "sectionHeadingColor": { - "type": "string" - }, - "fontFamily": { - "type": "string" - }, - "sectionSpacing": { - "type": "number" - }, - "pagePadding": { - "type": "number" - }, - "bodyLineHeight": { - "type": "number" - }, - "headingLineHeight": { - "type": "number" - } + "accentColor": { "type": "string" }, + "textColor": { "type": "string" }, + "mutedTextColor": { "type": "string" }, + "pageBackgroundColor": { "type": "string" }, + "sectionBackgroundColor": { "type": "string" }, + "borderColor": { "type": "string" }, + "sectionHeadingColor": { "type": "string" }, + "fontFamily": { "type": "string" }, + "sectionSpacing": { "type": "number" }, + "pagePadding": { "type": "number" }, + "bodyLineHeight": { "type": "number" }, + "headingLineHeight": { "type": "number" } } }, "MasterProfileContent": { @@ -6056,143 +4713,90 @@ "customization" ], "properties": { - "templateId": { - "type": "string" - }, - "basics": { - "$ref": "#/components/schemas/MasterProfileBasics" - }, + "templateId": { "type": "string" }, + "basics": { "$ref": "#/components/schemas/MasterProfileBasics" }, "links": { "type": "object", "properties": { - "displayMode": { - "type": "string", - "enum": ["icon", "url", "icon-username"] - }, + "displayMode": { "type": "string", "enum": ["icon", "url", "icon-username"] }, "items": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileLink" - } + "items": { "$ref": "#/components/schemas/MasterProfileLink" } } } }, - "summary": { - "type": "string" - }, + "summary": { "type": "string" }, "experience": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileExperienceItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileExperienceItem" } }, "education": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileEducationItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileEducationItem" } }, "projects": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileProjectItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileProjectItem" } }, "skills": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileSkillItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileSkillItem" } }, "languages": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileLanguageItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileLanguageItem" } }, "interests": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileInterestItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileInterestItem" } }, "awards": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileAwardItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileAwardItem" } }, "certificates": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileCertificateItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileCertificateItem" } }, "publications": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfilePublicationItem" - } + "items": { "$ref": "#/components/schemas/MasterProfilePublicationItem" } }, "volunteer": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileVolunteerItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileVolunteerItem" } }, "references": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileReferenceItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileReferenceItem" } }, "achievements": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileAchievementItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileAchievementItem" } }, "customSections": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileCustomSection" - } + "items": { "$ref": "#/components/schemas/MasterProfileCustomSection" } }, "sections": { "type": "array", - "items": { - "$ref": "#/components/schemas/MasterProfileSectionItem" - } + "items": { "$ref": "#/components/schemas/MasterProfileSectionItem" } }, - "customization": { - "$ref": "#/components/schemas/MasterProfileCustomization" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "nullable": true - } + "customization": { "$ref": "#/components/schemas/MasterProfileCustomization" }, + "updatedAt": { "type": "string", "format": "date-time", "nullable": true } } }, "MasterProfileResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "profile": { - "$ref": "#/components/schemas/MasterProfile" - }, - "summary": { - "$ref": "#/components/schemas/UserSummary" - } + "profile": { "$ref": "#/components/schemas/MasterProfile" }, + "summary": { "$ref": "#/components/schemas/UserSummary" } } } } @@ -6207,41 +4811,25 @@ "nullable": true, "description": "Pass the `updatedAt` value from your last read. Omit only when creating the profile for the first time." }, - "profile": { - "$ref": "#/components/schemas/MasterProfileContent" - } + "profile": { "$ref": "#/components/schemas/MasterProfileContent" } } }, "MasterProfileUpdateResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/MasterProfile" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/MasterProfile" } } }, "ProfileImportQuota": { "type": "object", "properties": { - "isPaid": { - "type": "boolean" - }, - "remaining": { - "type": "integer" - }, - "limit": { - "type": "integer" - }, - "resetsInSeconds": { - "type": "integer" - }, + "isPaid": { "type": "boolean" }, + "remaining": { "type": "integer" }, + "limit": { "type": "integer" }, + "resetsInSeconds": { "type": "integer" }, "connectedUsername": { "type": "string", "nullable": true, @@ -6253,21 +4841,13 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "github": { - "$ref": "#/components/schemas/ProfileImportQuota" - }, - "linkedin": { - "$ref": "#/components/schemas/ProfileImportQuota" - } + "github": { "$ref": "#/components/schemas/ProfileImportQuota" }, + "linkedin": { "$ref": "#/components/schemas/ProfileImportQuota" } } } } @@ -6307,12 +4887,8 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "$ref": "#/components/schemas/Document", "description": "The newly created resume Document seeded from the imported data." @@ -6323,55 +4899,18 @@ "type": "object", "required": ["id", "title", "description", "status", "createdAt", "updatedAt"], "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["todo", "in-progress", "done"] - }, - "eta": { - "type": "string", - "nullable": true - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "startedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "completedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "completedQuarter": { - "type": "string", - "nullable": true - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "timeline": { - "type": "string", - "nullable": true - } + "id": { "type": "string" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "status": { "type": "string", "enum": ["todo", "in-progress", "done"] }, + "eta": { "type": "string", "nullable": true }, + "tags": { "type": "array", "items": { "type": "string" } }, + "createdAt": { "type": "string", "format": "date-time" }, + "startedAt": { "type": "string", "format": "date-time", "nullable": true }, + "completedAt": { "type": "string", "format": "date-time", "nullable": true }, + "completedQuarter": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "format": "date-time" }, + "timeline": { "type": "string", "nullable": true } } }, "RoadmapListData": { @@ -6382,116 +4921,64 @@ "properties": { "items": { "type": "array", - "items": { - "$ref": "#/components/schemas/RoadmapFeature" - } + "items": { "$ref": "#/components/schemas/RoadmapFeature" } } } }, - { - "$ref": "#/components/schemas/OffsetPaginationMeta" - } + { "$ref": "#/components/schemas/OffsetPaginationMeta" } ] }, "RoadmapListResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/RoadmapListData" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/RoadmapListData" } } }, "RoadmapStats": { "type": "object", "properties": { - "totalFeatures": { - "type": "integer" - }, - "todo": { - "type": "integer" - }, - "inProgress": { - "type": "integer" - }, - "done": { - "type": "integer" - }, - "completionRate": { - "type": "string" - } + "totalFeatures": { "type": "integer" }, + "todo": { "type": "integer" }, + "inProgress": { "type": "integer" }, + "done": { "type": "integer" }, + "completionRate": { "type": "string" } } }, "RoadmapStatsResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/RoadmapStats" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/RoadmapStats" } } }, "RoadmapFeatureDetail": { "allOf": [ - { - "$ref": "#/components/schemas/RoadmapFeature" - }, + { "$ref": "#/components/schemas/RoadmapFeature" }, { "type": "object", "properties": { - "fullDescription": { - "type": "string", - "nullable": true - }, - "whyItMatters": { - "type": "string", - "nullable": true - }, - "details": { - "type": "object", - "nullable": true - }, + "fullDescription": { "type": "string", "nullable": true }, + "whyItMatters": { "type": "string", "nullable": true }, + "details": { "type": "object", "nullable": true }, "interactions": { "type": "array", "items": { "type": "object", "properties": { - "type": { - "type": "string" - }, - "value": { - "type": "integer" - }, - "comment": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, + "type": { "type": "string" }, + "value": { "type": "integer" }, + "comment": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "format": "date-time" }, "user": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string", - "nullable": true - } + "id": { "type": "string" }, + "name": { "type": "string", "nullable": true } } } } @@ -6505,40 +4992,143 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/RoadmapFeatureDetail" + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/RoadmapFeatureDetail" } + } + }, + "ChangelogPrRef": { + "type": "object", + "description": "A pull request referenced by a changelog entry, resolved from the GitHub release body.", + "required": ["number", "title"], + "properties": { + "number": { "type": "integer" }, + "title": { "type": "string" }, + "url": { "type": "string", "format": "uri" }, + "author": { + "type": "object", + "nullable": true, + "properties": { + "login": { "type": "string" }, + "avatarUrl": { "type": "string", "format": "uri" }, + "htmlUrl": { "type": "string", "format": "uri" } + } } } }, - "GitHubStats": { + "ChangelogEntry": { "type": "object", + "required": ["id", "version", "title", "type", "publishedAt"], "properties": { - "total": { - "type": "integer", - "description": "Issues plus pull requests." - }, - "issues": { - "type": "integer", - "description": "Issue-only count." - }, - "pullRequests": { - "type": "integer" - }, - "todo": { - "type": "integer" + "id": { "type": "string" }, + "version": { + "type": "string", + "description": "Semantic version string, unique across entries." + }, + "title": { "type": "string" }, + "summary": { "type": "string", "nullable": true }, + "type": { "type": "string", "enum": ["major", "minor", "patch"] }, + "publishedAt": { "type": "string", "format": "date-time" }, + "githubUrl": { "type": "string", "format": "uri", "nullable": true }, + "added": { "type": "array", "items": { "type": "string" } }, + "improved": { "type": "array", "items": { "type": "string" } }, + "fixed": { "type": "array", "items": { "type": "string" } }, + "breaking": { "type": "array", "items": { "type": "string" } }, + "security": { "type": "array", "items": { "type": "string" } }, + "tags": { "type": "array", "items": { "type": "string" } }, + "prRefs": { + "type": "array", + "nullable": true, + "items": { "$ref": "#/components/schemas/ChangelogPrRef" } + } + } + }, + "ChangelogListData": { + "allOf": [ + { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "$ref": "#/components/schemas/ChangelogEntry" } + } + } }, - "inProgress": { - "type": "integer" + { "$ref": "#/components/schemas/OffsetPaginationMeta" } + ] + }, + "ChangelogListResponse": { + "type": "object", + "required": ["success", "message", "data"], + "properties": { + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/ChangelogListData" } + } + }, + "ChangelogEntryResponse": { + "type": "object", + "required": ["success", "message", "data"], + "properties": { + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/ChangelogEntry" } + } + }, + "ChangelogStats": { + "type": "object", + "properties": { + "totalEntries": { "type": "integer" }, + "major": { "type": "integer" }, + "minor": { "type": "integer" }, + "patch": { "type": "integer" }, + "latest": { + "type": "object", + "nullable": true, + "description": "The most recently published entry, or `null` when the changelog is empty.", + "properties": { + "version": { "type": "string" }, + "title": { "type": "string" }, + "publishedAt": { "type": "string", "format": "date-time" } + } }, - "done": { - "type": "integer" + "contributorCount": { + "type": "integer", + "description": "Distinct pull-request authors across every entry." }, + "topContributors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "login": { "type": "string" }, + "avatarUrl": { "type": "string", "format": "uri" }, + "htmlUrl": { "type": "string", "format": "uri" }, + "contributions": { "type": "integer" } + } + } + } + } + }, + "ChangelogStatsResponse": { + "type": "object", + "required": ["success", "message", "data"], + "properties": { + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/ChangelogStats" } + } + }, + "GitHubStats": { + "type": "object", + "properties": { + "total": { "type": "integer", "description": "Issues plus pull requests." }, + "issues": { "type": "integer", "description": "Issue-only count." }, + "pullRequests": { "type": "integer" }, + "todo": { "type": "integer" }, + "inProgress": { "type": "integer" }, + "done": { "type": "integer" }, "completionRate": { "type": "string", "description": "Percentage of `total` that is done, formatted to 2 decimal places. `\"0.00\"` when there is nothing synced.", @@ -6549,77 +5139,34 @@ "GitHubStatsData": { "type": "object", "properties": { - "projectName": { - "type": "string" - }, - "stats": { - "$ref": "#/components/schemas/GitHubStats" - }, - "syncedAt": { - "type": "string", - "format": "date-time" - } + "projectName": { "type": "string" }, + "stats": { "$ref": "#/components/schemas/GitHubStats" }, + "syncedAt": { "type": "string", "format": "date-time" } } }, "GitHubStatsResponse": { "type": "object", "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "description": "Null until the first GitHub sync has run for the configured repository.", - "oneOf": [ - { - "$ref": "#/components/schemas/GitHubStatsData" - }, - { - "type": "null" - } - ] + "oneOf": [{ "$ref": "#/components/schemas/GitHubStatsData" }, { "type": "null" }] } } }, "GitHubItem": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "number": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["todo", "in-progress", "done"] - }, - "kind": { - "type": "string", - "enum": ["issue", "pull-request"] - }, - "url": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "id": { "type": "string" }, + "number": { "type": "integer" }, + "title": { "type": "string" }, + "status": { "type": "string", "enum": ["todo", "in-progress", "done"] }, + "kind": { "type": "string", "enum": ["issue", "pull-request"] }, + "url": { "type": "string" }, + "labels": { "type": "array", "items": { "type": "string" } }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "GitHubIssuesData": { @@ -6628,46 +5175,26 @@ "type": "object", "required": ["items"], "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitHubItem" - } - } + "items": { "type": "array", "items": { "$ref": "#/components/schemas/GitHubItem" } } } }, - { - "$ref": "#/components/schemas/OffsetPaginationMeta" - } + { "$ref": "#/components/schemas/OffsetPaginationMeta" } ] }, "GitHubIssuesResponse": { "type": "object", "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/GitHubIssuesData" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/GitHubIssuesData" } } }, "GenericSuccessResponse": { "type": "object", "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "additionalProperties": true - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "type": "object", "additionalProperties": true } } }, "NullDataResponse": { @@ -6675,16 +5202,9 @@ "description": "Success envelope for endpoints that acknowledge an action without returning a body.", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean", - "const": true - }, - "message": { - "type": "string" - }, - "data": { - "type": "null" - } + "success": { "type": "boolean", "const": true }, + "message": { "type": "string" }, + "data": { "type": "null" } } }, "ApiKeyScope": { @@ -6706,24 +5226,15 @@ "description": "The trimmed shape returned by `GET /api-keys`. The secret is **not** included in any form โ€”\nnot even masked. Use `keyPrefix` to identify a key in a list.\n", "required": ["id", "name", "keyPrefix", "isActive", "createdAt"], "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, + "id": { "type": "string" }, + "name": { "type": "string" }, "keyPrefix": { "type": "string", "description": "First 8 characters of the key (`vw_` plus 5 hex characters).", "example": "vw_a1b2c" }, - "isActive": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, + "isActive": { "type": "boolean" }, + "createdAt": { "type": "string", "format": "date-time" }, "lastUsed": { "type": "string", "format": "date-time", @@ -6750,65 +5261,32 @@ "updatedAt" ], "properties": { - "id": { - "type": "string" - }, + "id": { "type": "string" }, "key": { "type": "string", "description": "The full secret โ€” `vw_` followed by 64 hex characters. Shown once, never again.", "example": "vw_3f2a1c9e8b7d6f5a4c3b2a1908f7e6d5c4b3a29180706f5e4d3c2b1a09f8e7d6" }, - "keyPrefix": { - "type": "string", - "description": "First 8 characters of `key`." - }, - "keySuffix": { - "type": "string", - "description": "Last 8 characters of `key`." - }, - "name": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "isActive": { - "type": "boolean" - }, + "keyPrefix": { "type": "string", "description": "First 8 characters of `key`." }, + "keySuffix": { "type": "string", "description": "Last 8 characters of `key`." }, + "name": { "type": "string" }, + "userId": { "type": "string" }, + "isActive": { "type": "boolean" }, "rateLimit": { "type": "integer", "description": "Requests allowed per 15-minute window for this key." }, - "scopes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKeyScope" - } - }, + "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKeyScope" } }, "expiresAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Always set. Defaults to 365 days from creation when not supplied." }, - "revokedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "lastUsed": { - "type": "string", - "format": "date-time", - "nullable": true - }, + "revokedAt": { "type": "string", "format": "date-time", "nullable": true }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "lastUsed": { "type": "string", "format": "date-time", "nullable": true }, "rotatedFromId": { "type": "string", "description": "Only present on a rotate response โ€” the id of the key this one replaced." @@ -6819,12 +5297,8 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "allOf": [ { @@ -6832,15 +5306,11 @@ "properties": { "items": { "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKeyListItem" - } + "items": { "$ref": "#/components/schemas/ApiKeyListItem" } } } }, - { - "$ref": "#/components/schemas/OffsetPaginationMeta" - } + { "$ref": "#/components/schemas/OffsetPaginationMeta" } ] } } @@ -6849,17 +5319,62 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/ApiKeySecret" } + } + }, + "ApiKeyDetail": { + "type": "object", + "description": "The full metadata record for a single key, returned by `GET /api-keys/{id}`. Identical to\n`ApiKeySecret` minus `key` โ€” the secret itself is unrecoverable after creation.\n", + "required": [ + "id", + "keyPrefix", + "keySuffix", + "name", + "userId", + "isActive", + "rateLimit", + "scopes", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { "type": "string" }, + "keyPrefix": { "type": "string", "description": "First 8 characters of the key." }, + "keySuffix": { + "type": "string", + "description": "Last 8 characters of the key. Not enough to reconstruct the secret." }, - "message": { - "type": "string" + "name": { "type": "string" }, + "userId": { "type": "string" }, + "isActive": { "type": "boolean" }, + "rateLimit": { + "type": "integer", + "description": "Requests allowed per 15-minute window for this key." }, - "data": { - "$ref": "#/components/schemas/ApiKeySecret" + "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKeyScope" } }, + "expiresAt": { "type": "string", "format": "date-time", "nullable": true }, + "revokedAt": { "type": "string", "format": "date-time", "nullable": true }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "lastUsed": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Written at most once every 5 minutes per key, so it lags real usage slightly." } } }, + "ApiKeyDetailResponse": { + "type": "object", + "required": ["success", "message", "data"], + "properties": { + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/ApiKeyDetail" } + } + }, "Document": { "type": "object", "required": [ @@ -6878,67 +5393,34 @@ "updatedAt" ], "properties": { - "id": { - "type": "string" - }, - "userId": { - "type": "string" - }, + "id": { "type": "string" }, + "userId": { "type": "string" }, "type": { "type": "string", "enum": ["RESUME", "COVER_LETTER", "PORTFOLIO", "LINK_IN_BIO"] }, - "title": { - "type": "string" - }, + "title": { "type": "string" }, "slug": { "type": "string", "description": "Unique per user. Also used as the portfolio subdomain slug for PORTFOLIO documents." }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, + "tags": { "type": "array", "items": { "type": "string" } }, "content": { "type": "object", "additionalProperties": true, "description": "Document-type-specific JSON payload (resume schema, portfolio schema, etc)." }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true - }, - "templateId": { - "type": "string", - "default": "modern" - }, - "schemaVersion": { - "type": "integer" - }, + "metadata": { "type": "object", "nullable": true, "additionalProperties": true }, + "templateId": { "type": "string", "default": "modern" }, + "schemaVersion": { "type": "integer" }, "revision": { "type": "integer", "description": "Incremented on every update. Required (and checked) on PATCH for optimistic concurrency control." }, - "visibility": { - "type": "string", - "enum": ["PRIVATE", "UNLISTED", "PUBLIC"] - }, - "lastSyncedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, + "visibility": { "type": "string", "enum": ["PRIVATE", "UNLISTED", "PUBLIC"] }, + "lastSyncedAt": { "type": "string", "format": "date-time", "nullable": true }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, "deletedAt": { "type": "string", "format": "date-time", @@ -6951,33 +5433,18 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Document" - } - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "type": "array", "items": { "$ref": "#/components/schemas/Document" } } } }, "DocumentResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/Document" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/Document" } } }, "DocumentCreateRequest": { @@ -6992,71 +5459,34 @@ "type": "string", "enum": ["RESUME", "COVER_LETTER", "PORTFOLIO", "LINK_IN_BIO"] }, - "title": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, + "title": { "type": "string" }, + "slug": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, "content": { "type": "object", "additionalProperties": true, "description": "If omitted for RESUME/COVER_LETTER, content is auto-seeded from the user's MasterProfile when one exists." }, - "metadata": { - "type": "object", - "additionalProperties": true - }, - "templateId": { - "type": "string" - }, - "visibility": { - "type": "string", - "enum": ["PRIVATE", "UNLISTED", "PUBLIC"] - } + "metadata": { "type": "object", "additionalProperties": true }, + "templateId": { "type": "string" }, + "visibility": { "type": "string", "enum": ["PRIVATE", "UNLISTED", "PUBLIC"] } } }, "DocumentUpdateRequest": { "type": "object", "required": ["revision"], "properties": { - "title": { - "type": "string" - }, - "slug": { - "type": "string" - }, + "title": { "type": "string" }, + "slug": { "type": "string" }, "updateShareSlug": { "type": "boolean", "description": "When true and an active share link exists, its slug is updated to track the new document slug." }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "content": { - "type": "object", - "additionalProperties": true - }, - "metadata": { - "type": "object", - "additionalProperties": true - }, - "templateId": { - "type": "string" - }, - "visibility": { - "type": "string", - "enum": ["PRIVATE", "UNLISTED", "PUBLIC"] - }, + "tags": { "type": "array", "items": { "type": "string" } }, + "content": { "type": "object", "additionalProperties": true }, + "metadata": { "type": "object", "additionalProperties": true }, + "templateId": { "type": "string" }, + "visibility": { "type": "string", "enum": ["PRIVATE", "UNLISTED", "PUBLIC"] }, "revision": { "type": "integer", "description": "Must match the document's current revision. Mismatch returns 409." @@ -7066,112 +5496,47 @@ "AtsRuleResult": { "type": "object", "properties": { - "id": { - "type": "string" - }, + "id": { "type": "string" }, "category": { "type": "string", "description": "One of: parse, contact, structure, content, format." }, - "severity": { - "type": "string", - "enum": ["info", "warning", "error"] - }, - "passed": { - "type": "boolean" - }, - "evidence": { - "type": "string" - }, - "scoreImpact": { - "type": "integer" - }, - "fix": { - "type": "string" - } + "severity": { "type": "string", "enum": ["info", "warning", "error"] }, + "passed": { "type": "boolean" }, + "evidence": { "type": "string" }, + "scoreImpact": { "type": "integer" }, + "fix": { "type": "string" } } }, "AtsReport": { "description": "Deterministic, rule-based ATS report. Computed entirely server-side without calling any AI\nprovider. Shape depends on the caller's auth state: anonymous requests to `/ats/check` get\n`AtsRestrictedReport` (score + verdict only); any authenticated user (free or subscriber)\ngets `AtsFullReport`. `/ats/analyze` requires auth and always returns `AtsFullReport`.\n", "oneOf": [ - { - "$ref": "#/components/schemas/AtsFullReport" - }, - { - "$ref": "#/components/schemas/AtsRestrictedReport" - } + { "$ref": "#/components/schemas/AtsFullReport" }, + { "$ref": "#/components/schemas/AtsRestrictedReport" } ] }, "AtsFullReport": { "type": "object", "description": "Full deterministic report โ€” every rule's evidence, all matched/missing keywords, all prioritized fixes. Requires an authenticated caller.", "properties": { - "version": { - "type": "string", - "enum": ["ats-v2"] - }, - "restricted": { - "type": "boolean", - "enum": [false] - }, - "verdict": { - "type": "string", - "enum": ["strong", "needs-work", "weak"] - }, - "readinessScore": { - "type": "integer" - }, - "jobMatchScore": { - "type": "integer", - "nullable": true - }, - "matchedKeywords": { - "type": "array", - "items": { - "type": "string" - } - }, - "missingKeywords": { - "type": "array", - "items": { - "type": "string" - } - }, - "parsingWarnings": { - "type": "array", - "items": { - "type": "string" - } - }, - "strengths": { - "type": "array", - "items": { - "type": "string" - } - }, + "version": { "type": "string", "enum": ["ats-v2"] }, + "restricted": { "type": "boolean", "enum": [false] }, + "verdict": { "type": "string", "enum": ["strong", "needs-work", "weak"] }, + "readinessScore": { "type": "integer" }, + "jobMatchScore": { "type": "integer", "nullable": true }, + "matchedKeywords": { "type": "array", "items": { "type": "string" } }, + "missingKeywords": { "type": "array", "items": { "type": "string" } }, + "parsingWarnings": { "type": "array", "items": { "type": "string" } }, + "strengths": { "type": "array", "items": { "type": "string" } }, "failedChecks": { "type": "array", - "items": { - "$ref": "#/components/schemas/AtsRuleResult" - } - }, - "prioritizedFixes": { - "type": "array", - "items": { - "type": "string" - } - }, - "rules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AtsRuleResult" - } + "items": { "$ref": "#/components/schemas/AtsRuleResult" } }, + "prioritizedFixes": { "type": "array", "items": { "type": "string" } }, + "rules": { "type": "array", "items": { "$ref": "#/components/schemas/AtsRuleResult" } }, "categories": { "type": "array", - "items": { - "$ref": "#/components/schemas/AtsCategoryScore" - } + "items": { "$ref": "#/components/schemas/AtsCategoryScore" } }, "checksPassed": { "type": "integer", @@ -7199,18 +5564,9 @@ "type": "integer", "description": "Percentage of this category's own weight the resume kept (0-100)." }, - "passed": { - "type": "integer", - "description": "Rules in this category that passed." - }, - "total": { - "type": "integer", - "description": "Rules evaluated in this category." - }, - "lost": { - "type": "integer", - "description": "Points this category actually cost." - }, + "passed": { "type": "integer", "description": "Rules in this category that passed." }, + "total": { "type": "integer", "description": "Rules evaluated in this category." }, + "lost": { "type": "integer", "description": "Points this category actually cost." }, "possible": { "type": "integer", "description": "Worst-case points this category could have cost." @@ -7221,41 +5577,18 @@ "type": "object", "description": "Returned instead of AtsFullReport when /ats/check is called anonymously โ€” scores, a verdict,\nand the per-area rollup, but no rule evidence, no keyword lists, and only the single\nhighest-impact fix.\n", "properties": { - "version": { - "type": "string", - "enum": ["ats-v2"] - }, - "restricted": { - "type": "boolean", - "enum": [true] - }, - "readinessScore": { - "type": "integer" - }, - "jobMatchScore": { - "type": "integer", - "nullable": true - }, - "verdict": { - "type": "string", - "enum": ["strong", "needs-work", "weak"] - }, - "topFix": { - "type": "string", - "nullable": true - }, + "version": { "type": "string", "enum": ["ats-v2"] }, + "restricted": { "type": "boolean", "enum": [true] }, + "readinessScore": { "type": "integer" }, + "jobMatchScore": { "type": "integer", "nullable": true }, + "verdict": { "type": "string", "enum": ["strong", "needs-work", "weak"] }, + "topFix": { "type": "string", "nullable": true }, "categories": { "type": "array", - "items": { - "$ref": "#/components/schemas/AtsCategoryScore" - } - }, - "checksPassed": { - "type": "integer" - }, - "checksTotal": { - "type": "integer" + "items": { "$ref": "#/components/schemas/AtsCategoryScore" } }, + "checksPassed": { "type": "integer" }, + "checksTotal": { "type": "integer" }, "matchedKeywordCount": { "type": "integer", "description": "How many job-description terms the resume covers. The terms themselves require auth." @@ -7271,56 +5604,24 @@ "nullable": true, "description": "Null when the AI step was skipped (e.g. `/check`, which never calls AI).", "properties": { - "explanation": { - "type": "string" - }, - "missingEvidence": { - "type": "array", - "items": { - "type": "string" - } - }, - "keywordOpportunities": { - "type": "array", - "items": { - "type": "string" - } - }, - "recommendedImprovements": { - "type": "array", - "items": { - "type": "string" - } - }, - "priorityOrder": { - "type": "array", - "items": { - "type": "string" - } - } + "explanation": { "type": "string" }, + "missingEvidence": { "type": "array", "items": { "type": "string" } }, + "keywordOpportunities": { "type": "array", "items": { "type": "string" } }, + "recommendedImprovements": { "type": "array", "items": { "type": "string" } }, + "priorityOrder": { "type": "array", "items": { "type": "string" } } } }, "AtsQuotaSummary": { "type": "object", "properties": { - "tier": { - "type": "string", - "enum": ["anonymous", "free", "subscriber"] - }, + "tier": { "type": "string", "enum": ["anonymous", "free", "subscriber"] }, "limit": { "type": "integer", "description": "1 for anonymous callers per 48h window, 2 for free logged-in users per 24h window, 300 for subscribers per billing period." }, - "used": { - "type": "integer" - }, - "remaining": { - "type": "integer" - }, - "resetsAt": { - "type": "string", - "format": "date-time" - }, + "used": { "type": "integer" }, + "remaining": { "type": "integer" }, + "resetsAt": { "type": "string", "format": "date-time" }, "canConvertResume": { "type": "boolean", "description": "True only for active AI Credits / Bundle subscribers โ€” gates access to /ats/convert-resume." @@ -7330,44 +5631,22 @@ "properties": { "analysisCredits": { "type": "object", - "properties": { - "min": { - "type": "integer" - }, - "max": { - "type": "integer" - } - } + "properties": { "min": { "type": "integer" }, "max": { "type": "integer" } } }, "jobUrlAnalysisCredits": { "type": "object", - "properties": { - "min": { - "type": "integer" - }, - "max": { - "type": "integer" - } - } + "properties": { "min": { "type": "integer" }, "max": { "type": "integer" } } }, - "resumeConversionCredits": { - "type": "integer" - } + "resumeConversionCredits": { "type": "integer" } } }, "extract": { "type": "object", "description": "Separate budget for resume file uploads (POST /ats/extract) โ€” does not share the scan quota above.", "properties": { - "limit": { - "type": "integer" - }, - "used": { - "type": "integer" - }, - "remaining": { - "type": "integer" - } + "limit": { "type": "integer" }, + "used": { "type": "integer" }, + "remaining": { "type": "integer" } } } } @@ -7376,27 +5655,17 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/AtsQuotaSummary" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/AtsQuotaSummary" } } }, "AtsExtractResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { @@ -7414,15 +5683,7 @@ "properties": { "resume": { "description": "Resume text, or a JSON Resume-shaped object. Either is accepted.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "additionalProperties": true - } - ] + "oneOf": [{ "type": "string" }, { "type": "object", "additionalProperties": true }] }, "jobDescription": { "type": "string", @@ -7434,28 +5695,15 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "report": { - "$ref": "#/components/schemas/AtsReport" - }, - "ai": { - "type": "null" - }, - "creditsSpent": { - "type": "integer", - "enum": [0] - }, - "quota": { - "$ref": "#/components/schemas/AtsQuotaSummary" - } + "report": { "$ref": "#/components/schemas/AtsReport" }, + "ai": { "type": "null" }, + "creditsSpent": { "type": "integer", "enum": [0] }, + "quota": { "$ref": "#/components/schemas/AtsQuotaSummary" } } } } @@ -7465,28 +5713,15 @@ "required": ["resume", "requestId"], "properties": { "resume": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "additionalProperties": true - } - ] - }, - "jobDescription": { - "type": "string" + "oneOf": [{ "type": "string" }, { "type": "object", "additionalProperties": true }] }, + "jobDescription": { "type": "string" }, "jobUrl": { "type": "string", "format": "uri", "description": "Required when fetchJobUrl is true. Fetched server-side through an SSRF-hardened HTTPS-only fetcher (blocks localhost/private/link-local IPs, pins the resolved IP for the whole request, caps redirects to 3, response size to 2MB, and timeout to 8s)." }, - "fetchJobUrl": { - "type": "boolean", - "default": false - }, + "fetchJobUrl": { "type": "boolean", "default": false }, "requestId": { "type": "string", "minLength": 8, @@ -7498,28 +5733,16 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "description": "AI insights are drawn from the same shared ATS scan-quota bucket consumed by `/ats/check` โ€”\nthere is only one ATS quota, not separate \"scan\" and \"AI\" allowances.\n", "properties": { - "report": { - "$ref": "#/components/schemas/AtsReport" - }, - "ai": { - "$ref": "#/components/schemas/AtsAiInsights" - }, - "creditsSpent": { - "type": "integer" - }, - "quota": { - "$ref": "#/components/schemas/AtsQuotaSummary" - } + "report": { "$ref": "#/components/schemas/AtsReport" }, + "ai": { "$ref": "#/components/schemas/AtsAiInsights" }, + "creditsSpent": { "type": "integer" }, + "quota": { "$ref": "#/components/schemas/AtsQuotaSummary" } } } } @@ -7533,10 +5756,7 @@ "minLength": 1, "description": "Raw resume text to convert into structured JSON Resume fields." }, - "requestId": { - "type": "string", - "minLength": 8 - } + "requestId": { "type": "string", "minLength": 8 } } }, "AtsConvertedResume": { @@ -7546,89 +5766,47 @@ "basics": { "type": "object", "properties": { - "fullName": { - "type": "string" - }, - "role": { - "type": "string" - }, - "headline": { - "type": "string" - }, - "email": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "location": { - "type": "string" - } + "fullName": { "type": "string" }, + "role": { "type": "string" }, + "headline": { "type": "string" }, + "email": { "type": "string" }, + "phone": { "type": "string" }, + "location": { "type": "string" } } }, "links": { "type": "array", "items": { "type": "object", - "properties": { - "label": { - "type": "string" - }, - "url": { - "type": "string" - } - } + "properties": { "label": { "type": "string" }, "url": { "type": "string" } } } }, - "summary": { - "type": "string" - }, + "summary": { "type": "string" }, "experience": { "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } }, "education": { "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } }, "projects": { "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } }, - "skills": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } - } + "skills": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } }, "AtsConvertResumeResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "resume": { - "$ref": "#/components/schemas/AtsConvertedResume" - }, + "resume": { "$ref": "#/components/schemas/AtsConvertedResume" }, "creditsSpent": { "type": "integer", "description": "Requires an active AI Credits or Bundle entitlement โ€” this endpoint spends credits independently of the shared ATS scan quota above." @@ -7654,14 +5832,7 @@ "properties": { "costs": { "type": "object", - "properties": { - "standard": { - "type": "integer" - }, - "expert": { - "type": "integer" - } - } + "properties": { "standard": { "type": "integer" }, "expert": { "type": "integer" } } } } }, @@ -7669,31 +5840,15 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "description": "Map of AI action key to its credit cost per mode, containing an entry for every action in\n`AiActionKey`. The values below are illustrative only โ€” real costs come from server-side\nconfiguration, so read them from this endpoint rather than hardcoding them.\n", - "additionalProperties": { - "$ref": "#/components/schemas/AiActionCost" - }, + "additionalProperties": { "$ref": "#/components/schemas/AiActionCost" }, "example": { - "rewrite_short_text": { - "costs": { - "standard": 1, - "expert": 3 - } - }, - "generate_cover_letter": { - "costs": { - "standard": 4, - "expert": 10 - } - } + "rewrite_short_text": { "costs": { "standard": 1, "expert": 3 } }, + "generate_cover_letter": { "costs": { "standard": 4, "expert": 10 } } } } } @@ -7702,9 +5857,7 @@ "type": "object", "required": ["action", "input", "requestId"], "properties": { - "action": { - "$ref": "#/components/schemas/AiActionKey" - }, + "action": { "$ref": "#/components/schemas/AiActionKey" }, "mode": { "type": "string", "enum": ["standard", "expert"], @@ -7715,27 +5868,15 @@ "type": "object", "description": "Required, though every field inside it is optional and defaults to an empty string. At\nleast one of `text`, `context`, or `jobDescription` must be non-empty or the request fails\nwith `400`. Per-action character limits apply โ€” see `GET /ai/actions`.\n", "properties": { - "text": { - "type": "string", - "maxLength": 50000, - "default": "" - }, - "context": { - "type": "string", - "maxLength": 50000, - "default": "" - }, + "text": { "type": "string", "maxLength": 50000, "default": "" }, + "context": { "type": "string", "maxLength": 50000, "default": "" }, "jobDescription": { "type": "string", "maxLength": 20000, "default": "", "description": "Silently truncated to the action's own job-description limit rather than rejected." }, - "instructions": { - "type": "string", - "maxLength": 500, - "default": "" - } + "instructions": { "type": "string", "maxLength": 500, "default": "" } } }, "requestId": { @@ -7756,39 +5897,25 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "content": { - "type": "string" - }, + "content": { "type": "string" }, "usage": { "type": "object", "nullable": true, "properties": { - "promptTokens": { - "type": "integer" - }, - "completionTokens": { - "type": "integer" - }, - "totalTokens": { - "type": "integer" - } + "promptTokens": { "type": "integer" }, + "completionTokens": { "type": "integer" }, + "totalTokens": { "type": "integer" } } }, "credits": { "type": "object", "properties": { - "spent": { - "type": "integer" - }, + "spent": { "type": "integer" }, "balance": { "type": "integer", "description": "AI credit wallet balance after this generation." @@ -7802,59 +5929,35 @@ "PortfolioPublicSummary": { "type": "object", "properties": { - "subdomain": { - "type": "string" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "subdomain": { "type": "string" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "PortfolioPublicListResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "array", - "items": { - "$ref": "#/components/schemas/PortfolioPublicSummary" - } + "items": { "$ref": "#/components/schemas/PortfolioPublicSummary" } } } }, "PortfolioPublication": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "subdomain": { - "type": "string" - }, - "templateId": { - "type": "string", - "enum": ["signal", "atelier", "nimbus", "cipher"] - }, + "id": { "type": "string" }, + "subdomain": { "type": "string" }, + "templateId": { "type": "string", "enum": ["signal", "atelier", "nimbus", "cipher"] }, "snapshot": { "type": "object", "additionalProperties": true, "description": "Published portfolio content snapshot (validated against the portfolio content schema at publish time)." }, - "status": { - "type": "string", - "enum": ["LIVE", "GRACE", "SUSPENDED"] - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, + "status": { "type": "string", "enum": ["LIVE", "GRACE", "SUSPENDED"] }, + "updatedAt": { "type": "string", "format": "date-time" }, "isPremium": { "type": "boolean", "description": "Whether the owner currently has an active portfolio_pro/bundle subscription." @@ -7865,15 +5968,9 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/PortfolioPublication" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/PortfolioPublication" } } }, "PortfolioPublicationRecord": { @@ -7892,30 +5989,13 @@ "updatedAt" ], "properties": { - "id": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "documentId": { - "type": "string" - }, - "subdomain": { - "type": "string" - }, - "templateId": { - "type": "string", - "enum": ["signal", "atelier", "nimbus", "cipher"] - }, - "snapshot": { - "type": "object", - "additionalProperties": true - }, - "status": { - "type": "string", - "enum": ["LIVE", "GRACE", "SUSPENDED"] - }, + "id": { "type": "string" }, + "userId": { "type": "string" }, + "documentId": { "type": "string" }, + "subdomain": { "type": "string" }, + "templateId": { "type": "string", "enum": ["signal", "atelier", "nimbus", "cipher"] }, + "snapshot": { "type": "object", "additionalProperties": true }, + "status": { "type": "string", "enum": ["LIVE", "GRACE", "SUSPENDED"] }, "publishedRevision": { "type": "integer", "description": "The draft `revision` that was published." @@ -7925,36 +6005,20 @@ "nullable": true, "description": "One of `grace_expired` or `user_unpublished` when the portfolio is suspended." }, - "suspendedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "publishedAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "suspendedAt": { "type": "string", "format": "date-time", "nullable": true }, + "publishedAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "PortfolioPublishResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "allOf": [ - { - "$ref": "#/components/schemas/PortfolioPublicationRecord" - }, + { "$ref": "#/components/schemas/PortfolioPublicationRecord" }, { "type": "object", "required": ["publicUrl"], @@ -7974,21 +6038,13 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "description": "The updated publication record with `status: SUSPENDED`, or null when the caller had never\npublished a portfolio. No `publicUrl` is included.\n", "oneOf": [ - { - "$ref": "#/components/schemas/PortfolioPublicationRecord" - }, - { - "type": "null" - } + { "$ref": "#/components/schemas/PortfolioPublicationRecord" }, + { "type": "null" } ] } } @@ -7998,17 +6054,9 @@ "description": "A trimmed projection of the caller's PORTFOLIO-type Document โ€” not a full `Document`. Only the\nfields below are selected.\n", "required": ["id", "slug", "templateId", "content", "revision", "updatedAt"], "properties": { - "id": { - "type": "string" - }, - "slug": { - "type": "string", - "description": "Kept in sync with the portfolio subdomain." - }, - "templateId": { - "type": "string", - "enum": ["signal", "atelier", "nimbus", "cipher"] - }, + "id": { "type": "string" }, + "slug": { "type": "string", "description": "Kept in sync with the portfolio subdomain." }, + "templateId": { "type": "string", "enum": ["signal", "atelier", "nimbus", "cipher"] }, "content": { "type": "object", "additionalProperties": true, @@ -8018,25 +6066,16 @@ "type": "integer", "description": "Send this back as `revision` when saving or publishing." }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "updatedAt": { "type": "string", "format": "date-time" } } }, "PortfolioDraftResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/PortfolioDraft" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/PortfolioDraft" } } }, "PortfolioPreview": { @@ -8044,39 +6083,20 @@ "description": "Same projection as `PortfolioDraft` minus `slug`.", "required": ["id", "templateId", "content", "revision", "updatedAt"], "properties": { - "id": { - "type": "string" - }, - "templateId": { - "type": "string", - "enum": ["signal", "atelier", "nimbus", "cipher"] - }, - "content": { - "type": "object", - "additionalProperties": true - }, - "revision": { - "type": "integer" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "id": { "type": "string" }, + "templateId": { "type": "string", "enum": ["signal", "atelier", "nimbus", "cipher"] }, + "content": { "type": "object", "additionalProperties": true }, + "revision": { "type": "integer" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "PortfolioPreviewResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/PortfolioPreview" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/PortfolioPreview" } } }, "PortfolioMe": { @@ -8084,38 +6104,17 @@ "properties": { "draft": { "description": "Null when the caller has never saved a portfolio draft.", - "oneOf": [ - { - "$ref": "#/components/schemas/PortfolioDraft" - }, - { - "type": "null" - } - ] + "oneOf": [{ "$ref": "#/components/schemas/PortfolioDraft" }, { "type": "null" }] }, "publication": { "type": "object", "nullable": true, "properties": { - "subdomain": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["LIVE", "GRACE", "SUSPENDED"] - }, - "publishedRevision": { - "type": "integer" - }, - "publishedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "subdomain": { "type": "string" }, + "status": { "type": "string", "enum": ["LIVE", "GRACE", "SUSPENDED"] }, + "publishedRevision": { "type": "integer" }, + "publishedAt": { "type": "string", "format": "date-time", "nullable": true }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "billing": { @@ -8129,15 +6128,9 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/PortfolioMe" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/PortfolioMe" } } }, "PortfolioSaveDraftRequest": { @@ -8148,10 +6141,7 @@ "type": "string", "description": "Omit to create (or continue) the user's single PORTFOLIO document." }, - "subdomain": { - "type": "string", - "maxLength": 63 - }, + "subdomain": { "type": "string", "maxLength": 63 }, "revision": { "type": "integer", "description": "Required once a draft already exists, for optimistic concurrency (409 on mismatch)." @@ -8167,13 +6157,8 @@ "type": "object", "required": ["documentId", "subdomain", "revision"], "properties": { - "documentId": { - "type": "string" - }, - "subdomain": { - "type": "string", - "maxLength": 63 - }, + "documentId": { "type": "string" }, + "subdomain": { "type": "string", "maxLength": 63 }, "revision": { "type": "integer", "description": "Must match the draft's current revision (save the latest draft first)." @@ -8183,21 +6168,14 @@ "PortfolioAnalytics": { "type": "object", "properties": { - "totalViews": { - "type": "integer" - }, + "totalViews": { "type": "integer" }, "daily": { "type": "array", "items": { "type": "object", "properties": { - "date": { - "type": "string", - "format": "date-time" - }, - "count": { - "type": "integer" - } + "date": { "type": "string", "format": "date-time" }, + "count": { "type": "integer" } } } }, @@ -8205,14 +6183,7 @@ "type": "array", "items": { "type": "object", - "properties": { - "host": { - "type": "string" - }, - "count": { - "type": "integer" - } - } + "properties": { "host": { "type": "string" }, "count": { "type": "integer" } } } } } @@ -8221,37 +6192,20 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/PortfolioAnalytics" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/PortfolioAnalytics" } } }, "PortfolioSubdomainAvailabilityResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", - "properties": { - "subdomain": { - "type": "string" - }, - "available": { - "type": "boolean" - } - } + "properties": { "subdomain": { "type": "string" }, "available": { "type": "boolean" } } } } }, @@ -8259,14 +6213,8 @@ "type": "object", "required": ["kind", "mimeType", "sizeBytes"], "properties": { - "kind": { - "type": "string", - "enum": ["AVATAR", "PROJECT_COVER", "SOCIAL_IMAGE"] - }, - "mimeType": { - "type": "string", - "enum": ["image/jpeg", "image/png", "image/webp"] - }, + "kind": { "type": "string", "enum": ["AVATAR", "PROJECT_COVER", "SOCIAL_IMAGE"] }, + "mimeType": { "type": "string", "enum": ["image/jpeg", "image/png", "image/webp"] }, "sizeBytes": { "type": "integer", "maximum": 5242880, @@ -8278,26 +6226,17 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "assetId": { - "type": "string" - }, + "assetId": { "type": "string" }, "uploadUrl": { "type": "string", "description": "Presigned Cloudflare R2 PUT URL. Upload the file directly to this URL with a matching Content-Type/Content-Length." }, - "expiresInSeconds": { - "type": "integer", - "enum": [600] - } + "expiresInSeconds": { "type": "integer", "enum": [600] } } } } @@ -8306,9 +6245,7 @@ "type": "object", "required": ["assetId"], "properties": { - "assetId": { - "type": "string" - }, + "assetId": { "type": "string" }, "checksum": { "type": "string", "description": "Optional ETag to verify against the uploaded object." @@ -8319,18 +6256,12 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "id": { - "type": "string" - }, + "id": { "type": "string" }, "url": { "type": "string", "description": "Public CDN URL. Only returned after a server-side HEAD request against R2 confirms the\nuploaded object's size, content type, and (optionally) checksum match what was declared\nwhen the upload URL was requested.\n" @@ -8342,21 +6273,13 @@ "BillingSummary": { "type": "object", "properties": { - "plan": { - "type": "string", - "enum": ["FREE", "AI_CREDITS", "PORTFOLIO_PRO", "BUNDLE"] - }, + "plan": { "type": "string", "enum": ["FREE", "AI_CREDITS", "PORTFOLIO_PRO", "BUNDLE"] }, "productKey": { "type": "string", "nullable": true, "enum": ["ai_credits", "portfolio_pro", "bundle", null] }, - "activeProductKeys": { - "type": "array", - "items": { - "type": "string" - } - }, + "activeProductKeys": { "type": "array", "items": { "type": "string" } }, "status": { "type": "string", "description": "Subscription status (e.g. ACTIVE, TRIALING, INACTIVE)." @@ -8366,65 +6289,29 @@ "nullable": true, "enum": ["one_day", "seven_day", "monthly", "annual", null] }, - "currentPeriodEnd": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelAtPeriodEnd": { - "type": "boolean" - }, - "graceEndsAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, + "currentPeriodEnd": { "type": "string", "format": "date-time", "nullable": true }, + "cancelAtPeriodEnd": { "type": "boolean" }, + "graceEndsAt": { "type": "string", "format": "date-time", "nullable": true }, "canPublish": { "type": "boolean", "description": "Portfolio-publish entitlement is a separate system from the AI credit wallet below." }, - "eligibleForTrial": { - "type": "boolean" - }, - "accessEndsAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "publicationStatus": { - "type": "string", - "enum": ["LIVE", "GRACE", "SUSPENDED"] - }, - "entitlements": { - "type": "array", - "items": { - "type": "string" - } - }, - "credits": { - "$ref": "#/components/schemas/CreditWallet" - }, + "eligibleForTrial": { "type": "boolean" }, + "accessEndsAt": { "type": "string", "format": "date-time", "nullable": true }, + "publicationStatus": { "type": "string", "enum": ["LIVE", "GRACE", "SUSPENDED"] }, + "entitlements": { "type": "array", "items": { "type": "string" } }, + "credits": { "$ref": "#/components/schemas/CreditWallet" }, "catalog": { "type": "object", "additionalProperties": true, "description": "Public product/pricing catalog (prices, intervals, entitlements per plan)." }, - "creditEconomics": { - "type": "object", - "additionalProperties": true - }, + "creditEconomics": { "type": "object", "additionalProperties": true }, "addOns": { "type": "array", "items": { "type": "object", - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - } - } + "properties": { "key": { "type": "string" }, "name": { "type": "string" } } } } } @@ -8433,140 +6320,77 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/BillingSummary" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/BillingSummary" } } }, "BillingHistoryEvent": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "providerEventId": { - "type": "string" - }, - "type": { - "type": "string" - }, - "processedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } + "id": { "type": "string" }, + "providerEventId": { "type": "string" }, + "type": { "type": "string" }, + "processedAt": { "type": "string", "format": "date-time", "nullable": true }, + "createdAt": { "type": "string", "format": "date-time" } } }, "BillingHistoryResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "array", - "items": { - "$ref": "#/components/schemas/BillingHistoryEvent" - } + "items": { "$ref": "#/components/schemas/BillingHistoryEvent" } } } }, "CreditWallet": { "type": "object", "properties": { - "balance": { - "type": "integer" - }, + "balance": { "type": "integer" }, "reserved": { "type": "integer", "description": "Credits currently held by in-flight AI generations that have not yet committed or been released." }, - "lifetimeCredited": { - "type": "integer" - }, - "lifetimeDebited": { - "type": "integer" - }, - "nextExpiryAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "nextExpiryCredits": { - "type": "integer" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "lifetimeCredited": { "type": "integer" }, + "lifetimeDebited": { "type": "integer" }, + "nextExpiryAt": { "type": "string", "format": "date-time", "nullable": true }, + "nextExpiryCredits": { "type": "integer" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "CreditWalletResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/CreditWallet" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/CreditWallet" } } }, "CreditHistoryEntry": { "type": "object", "additionalProperties": true, "properties": { - "id": { - "type": "string" - }, - "action": { - "type": "string" - }, - "cost": { - "type": "integer" - }, - "balanceAfter": { - "type": "integer" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } + "id": { "type": "string" }, + "action": { "type": "string" }, + "cost": { "type": "integer" }, + "balanceAfter": { "type": "integer" }, + "createdAt": { "type": "string", "format": "date-time" } } }, "CreditHistoryResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "array", - "items": { - "$ref": "#/components/schemas/CreditHistoryEntry" - } + "items": { "$ref": "#/components/schemas/CreditHistoryEntry" } } } }, @@ -8580,10 +6404,7 @@ "default": "portfolio_pro", "description": "Validated against a fixed product/interval whitelist before being handed off to the Dodo Payments checkout API." }, - "interval": { - "type": "string", - "enum": ["one_day", "seven_day", "monthly", "annual"] - }, + "interval": { "type": "string", "enum": ["one_day", "seven_day", "monthly", "annual"] }, "redirectUrl": { "type": "string", "description": "Relative path (must start with a single `/`) to return to after checkout." @@ -8594,12 +6415,8 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { @@ -8615,39 +6432,21 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "type": "object", "properties": { "success": { "type": "boolean" } } } } }, "BillingPortalResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "url": { - "type": "string", - "description": "Dodo Payments customer portal URL." - } + "url": { "type": "string", "description": "Dodo Payments customer portal URL." } } } } @@ -8656,10 +6455,7 @@ "type": "object", "required": ["packKey"], "properties": { - "packKey": { - "type": "string", - "enum": ["credit_pack_250", "credit_pack_500"] - }, + "packKey": { "type": "string", "enum": ["credit_pack_250", "credit_pack_500"] }, "redirectUrl": { "type": "string", "description": "Relative path (must start with a single `/`) to return to after checkout." @@ -8670,12 +6466,8 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { @@ -8690,28 +6482,16 @@ "ShareLink": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "documentId": { - "type": "string" - }, - "slug": { - "type": "string" - }, + "id": { "type": "string" }, + "userId": { "type": "string" }, + "documentId": { "type": "string" }, + "slug": { "type": "string" }, "token": { "type": "string", "description": "Same value as `slug` โ€” the public path segment for this link." }, - "username": { - "type": "string" - }, - "documentSlug": { - "type": "string" - }, + "username": { "type": "string" }, + "documentSlug": { "type": "string" }, "publicPath": { "type": "string", "description": "Convenience path built from the owner's username and the link slug. Only returned by\n`POST /shares`, not by the list endpoint.\n", @@ -8726,36 +6506,18 @@ "type": "boolean", "description": "The scrypt password hash itself is never returned in any API response." }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "viewCount": { - "type": "integer" - }, - "lastViewedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } + "expiresAt": { "type": "string", "format": "date-time", "nullable": true }, + "viewCount": { "type": "integer" }, + "lastViewedAt": { "type": "string", "format": "date-time", "nullable": true }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } } }, "ShareLinkCreateRequest": { "type": "object", "required": ["documentId", "snapshot"], "properties": { - "documentId": { - "type": "string" - }, + "documentId": { "type": "string" }, "snapshot": { "type": "object", "additionalProperties": true, @@ -8765,48 +6527,30 @@ "type": "string", "description": "When provided, the link is scrypt-hashed and requires this password to view." }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "noExpiry": { - "type": "boolean" - }, + "expiresAt": { "type": "string", "format": "date-time", "nullable": true }, + "noExpiry": { "type": "boolean" }, "updateSlug": { "type": "boolean", "description": "Regenerate the public slug instead of reusing the existing one (if a link already exists for this document)." }, - "removePassword": { - "type": "boolean" - } + "removePassword": { "type": "boolean" } } }, "ShareLinkResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/ShareLink" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/ShareLink" } } }, "ShareLinkListResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "allOf": [ { @@ -8815,15 +6559,11 @@ "properties": { "items": { "type": "array", - "items": { - "$ref": "#/components/schemas/ShareLink" - } + "items": { "$ref": "#/components/schemas/ShareLink" } } } }, - { - "$ref": "#/components/schemas/OffsetPaginationMeta" - } + { "$ref": "#/components/schemas/OffsetPaginationMeta" } ] } } @@ -8832,42 +6572,21 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", - "properties": { - "documentIds": { - "type": "array", - "items": { - "type": "string" - } - } - } + "properties": { "documentIds": { "type": "array", "items": { "type": "string" } } } } } }, "PublicSharePayload": { "type": "object", "properties": { - "passwordRequired": { - "type": "boolean" - }, - "resumeTitle": { - "type": "string" - }, - "documentTitle": { - "type": "string" - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, + "passwordRequired": { "type": "boolean" }, + "resumeTitle": { "type": "string" }, + "documentTitle": { "type": "string" }, + "expiresAt": { "type": "string", "format": "date-time", "nullable": true }, "snapshot": { "type": "object", "additionalProperties": true, @@ -8879,96 +6598,48 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/PublicSharePayload" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/PublicSharePayload" } } }, "VerifySharePasswordRequest": { "type": "object", "required": ["password"], - "properties": { - "password": { - "type": "string", - "minLength": 1 - } - } + "properties": { "password": { "type": "string", "minLength": 1 } } }, "AffiliateClickRequest": { "type": "object", "required": ["code"], "properties": { - "code": { - "type": "string", - "minLength": 4, - "maxLength": 32 - }, - "referrerHost": { - "type": "string" - } + "code": { "type": "string", "minLength": 4, "maxLength": 32 }, + "referrerHost": { "type": "string" } } }, "AffiliateClickResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "tracked": { - "type": "boolean" - } - } - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "type": "object", "properties": { "tracked": { "type": "boolean" } } } } }, "AffiliateLeaderboardEntry": { "type": "object", "properties": { - "rank": { - "type": "integer" - }, - "earningsCents": { - "type": "integer" - }, - "commissions": { - "type": "integer" - }, + "rank": { "type": "integer" }, + "earningsCents": { "type": "integer" }, + "commissions": { "type": "integer" }, "user": { "type": "object", "nullable": true, "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string", - "nullable": true - }, - "username": { - "type": "string", - "nullable": true - }, - "image": { - "type": "string", - "nullable": true - }, - "affiliateTier": { - "type": "string", - "enum": ["TIER_1", "TIER_2", "TIER_3"] - } + "id": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "username": { "type": "string", "nullable": true }, + "image": { "type": "string", "nullable": true }, + "affiliateTier": { "type": "string", "enum": ["TIER_1", "TIER_2", "TIER_3"] } } } } @@ -8977,17 +6648,11 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "array", - "items": { - "$ref": "#/components/schemas/AffiliateLeaderboardEntry" - } + "items": { "$ref": "#/components/schemas/AffiliateLeaderboardEntry" } } } }, @@ -8998,95 +6663,47 @@ "type": "string", "enum": ["NOT_ENROLLED", "PENDING", "ACTIVE", "SUSPENDED"] }, - "affiliateTier": { - "type": "string", - "enum": ["TIER_1", "TIER_2", "TIER_3"] - }, - "affiliateCode": { - "type": "string", - "nullable": true - }, - "affiliateEnrolledAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "clicks": { - "type": "integer" - }, + "affiliateTier": { "type": "string", "enum": ["TIER_1", "TIER_2", "TIER_3"] }, + "affiliateCode": { "type": "string", "nullable": true }, + "affiliateEnrolledAt": { "type": "string", "format": "date-time", "nullable": true }, + "clicks": { "type": "integer" }, "referrals": { "type": "array", "description": "Most recent 50 referrals (recency-limited preview). Use totalReferrals/conversions for exact counts.", "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["PENDING", "CONVERTED"] - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "convertedAt": { - "type": "string", - "format": "date-time", - "nullable": true - } + "id": { "type": "string" }, + "status": { "type": "string", "enum": ["PENDING", "CONVERTED"] }, + "createdAt": { "type": "string", "format": "date-time" }, + "convertedAt": { "type": "string", "format": "date-time", "nullable": true } } } }, - "totalReferrals": { - "type": "integer" - }, - "conversions": { - "type": "integer" - }, + "totalReferrals": { "type": "integer" }, + "conversions": { "type": "integer" }, "commissions": { "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } }, "withdrawals": { "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } }, "wallet": { "type": "object", "properties": { - "pendingCents": { - "type": "integer" - }, - "availableCents": { - "type": "integer" - }, - "paidCents": { - "type": "integer" - } + "pendingCents": { "type": "integer" }, + "availableCents": { "type": "integer" }, + "paidCents": { "type": "integer" } } }, - "minimumWithdrawalCents": { - "type": "integer", - "enum": [2500] - }, + "minimumWithdrawalCents": { "type": "integer", "enum": [2500] }, "tierProgress": { "type": "object", "properties": { - "currentConversions": { - "type": "integer" - }, - "nextTierConversions": { - "type": "integer", - "nullable": true - } + "currentConversions": { "type": "integer" }, + "nextTierConversions": { "type": "integer", "nullable": true } } }, "tiers": { @@ -9094,24 +6711,11 @@ "items": { "type": "object", "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "rateBps": { - "type": "integer" - }, - "requiredConversions": { - "type": "integer" - }, - "perks": { - "type": "array", - "items": { - "type": "string" - } - } + "key": { "type": "string" }, + "name": { "type": "string" }, + "rateBps": { "type": "integer" }, + "requiredConversions": { "type": "integer" }, + "perks": { "type": "array", "items": { "type": "string" } } } } } @@ -9121,15 +6725,9 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/AffiliateDashboard" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/AffiliateDashboard" } } }, "AffiliateReferralRequest": { @@ -9148,16 +6746,9 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "additionalProperties": true - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "type": "object", "additionalProperties": true } } }, "AffiliateWithdrawalRequest": { @@ -9176,12 +6767,8 @@ "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "additionalProperties": true, @@ -9193,67 +6780,32 @@ "type": "object", "required": ["collegeName", "graduationYear", "whyJoin", "superpower", "funFact"], "properties": { - "collegeName": { - "type": "string", - "minLength": 2, - "maxLength": 160 - }, + "collegeName": { "type": "string", "minLength": 2, "maxLength": 160 }, "graduationYear": { "type": "string", "pattern": "^(19|20)\\d{2}$", "description": "A real 4-digit year, validated both client- and server-side." }, - "whyJoin": { - "type": "string", - "minLength": 20, - "maxLength": 1000 - }, - "superpower": { - "type": "string", - "minLength": 2, - "maxLength": 300 - }, - "funFact": { - "type": "string", - "minLength": 2, - "maxLength": 300 - }, - "vibeCheck": { - "type": "string", - "maxLength": 80 - }, - "socialHandle": { - "type": "string", - "maxLength": 120 - } + "whyJoin": { "type": "string", "minLength": 20, "maxLength": 1000 }, + "superpower": { "type": "string", "minLength": 2, "maxLength": 300 }, + "funFact": { "type": "string", "minLength": 2, "maxLength": 300 }, + "vibeCheck": { "type": "string", "maxLength": 80 }, + "socialHandle": { "type": "string", "maxLength": 120 } } }, "AmbassadorApplicationResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, + "success": { "type": "boolean" }, + "message": { "type": "string" }, "data": { "type": "object", "properties": { - "success": { - "type": "boolean" - }, - "ambassadorStatus": { - "type": "string", - "enum": ["PENDING", "APPROVED", "REJECTED"] - }, - "collegeName": { - "type": "string" - }, - "graduationYear": { - "type": "string" - } + "success": { "type": "boolean" }, + "ambassadorStatus": { "type": "string", "enum": ["PENDING", "APPROVED", "REJECTED"] }, + "collegeName": { "type": "string" }, + "graduationYear": { "type": "string" } } } } @@ -9270,29 +6822,17 @@ "type": "string", "enum": ["NONE", "PENDING", "APPROVED", "REJECTED"] }, - "collegeName": { - "type": "string", - "nullable": true - }, - "graduationYear": { - "type": "string", - "nullable": true - } + "collegeName": { "type": "string", "nullable": true }, + "graduationYear": { "type": "string", "nullable": true } } }, "AmbassadorStatusResponse": { "type": "object", "required": ["success", "message", "data"], "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/AmbassadorStatus" - } + "success": { "type": "boolean" }, + "message": { "type": "string" }, + "data": { "$ref": "#/components/schemas/AmbassadorStatus" } } } }, @@ -9300,55 +6840,35 @@ "ValidationError": { "description": "Bad Request - The request body or parameters are invalid.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "UnauthorizedError": { "description": "Unauthorized - Authentication failed or session expired.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "statusCode": 401, - "message": "Authentication required" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, + "example": { "success": false, "statusCode": 401, "message": "Authentication required" } } } }, "BadRequestError": { "description": "Bad Request - The request parameters or body are invalid.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "NotFoundError": { "description": "Not Found - The requested resource does not exist.", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "FeatureDisabledError": { "description": "Service Unavailable - This feature is gated behind a feature flag that is currently off\n(`config.growth.affiliateProgramEnabled` / `config.growth.ambassadorProgramEnabled`), which is\nthe default in production until the program is explicitly enabled.\n", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, + "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "success": false, "statusCode": 503, diff --git a/apps/docs-platform/content/docs/architecture/monorepo.mdx b/apps/docs-platform/content/docs/architecture/monorepo.mdx index 196907cd..a8018971 100644 --- a/apps/docs-platform/content/docs/architecture/monorepo.mdx +++ b/apps/docs-platform/content/docs/architecture/monorepo.mdx @@ -96,8 +96,7 @@ reference. `VeriWorkly/portfolio-templates` repository, pinned to a specific upstream commit. It holds the four portfolio templates (Signal, Atelier, Nimbus, Cipher) and their registry. -Without access to that repository, `apps/portfolio` cannot build. Every other workspace is -unaffected. +Without access to that repository, you can scaffold stand-in mock templates using `node scripts/mock-template-library.mjs` to build and test `apps/portfolio` locally. Every other workspace builds and runs without it. --- diff --git a/apps/docs-platform/content/docs/architecture/state-management.mdx b/apps/docs-platform/content/docs/architecture/state-management.mdx index d7122316..5e9c5a64 100644 --- a/apps/docs-platform/content/docs/architecture/state-management.mdx +++ b/apps/docs-platform/content/docs/architecture/state-management.mdx @@ -26,12 +26,13 @@ Studio uses **Zustand** without persistence middleware. Persistence is explicit: `saveToStorage` / `hydrateFromStorage` actions that call dedicated service modules, so writes are deliberate rather than a side effect of every state change. -| Store | Location | Holds | -| :--------------- | :-------------------------------------- | :------------------------------------------------------------------------------- | -| `useResumeStore` | `features/resume/store/resume-store.ts` | The **single active** `ResumeData` document plus the currently selected section. | -| `useUserStore` | `store/useUserStore.ts` | The session user, login state, and loading flag. | +| Store | Location | Holds | +| :-------------------- | :-------------------------------------------------- | :------------------------------------------------------------------------------- | +| `useResumeStore` | `features/resume/store/resume-store.ts` | The **single active** `ResumeData` document plus the currently selected section. | +| `useCoverLetterStore` | `features/cover-letter/store/cover-letter-store.ts` | The **single active** `CoverLetterDocument` plus the currently selected section. | +| `useUserStore` | `store/useUserStore.ts` | The session user, login state, and loading flag. | -`useResumeStore` holds one document at a time โ€” not a collection. The multi-document library is not +`useResumeStore` and `useCoverLetterStore` hold one document at a time โ€” not a collection. The multi-document library is not store state; it is read from and written to `localStorage` through `features/documents/services/document-library.ts`. @@ -65,15 +66,24 @@ malformed or partially-migrated data is repaired on the way in rather than persi All document persistence targets the browser's **`localStorage`**. -Keys follow a single versioned scheme owned by +Keys follow a versioned scheme owned by `features/documents/services/storage-keys.ts`: -| Key | Contents | -| :-------------------------- | :-------------------------------------------------------------------- | -| `veriworkly:docs:v2:active` | The currently open document. | -| `veriworkly:docs:v2:{type}` | The document collection for a type (`resume`, `cover_letter`, โ€ฆ). | -| `veriworkly:sync-outbox` | Pending sync work items. | -| `veriworkly:sync-telemetry` | Last attempt / success / error timestamps and the last error message. | +### v3 Storage Layout (Per-Document Keys + Metadata Index) + +The v2 storage scheme stored every document of a type inside a single JSON blob. That made every write scale with total library size and caused cross-tab clobbering. The **v3 scheme** isolates every document and decouples metadata indexing: + +| Key | Contents | +| :----------------------------------- | :------------------------------------------------------------------------------------------------ | +| `veriworkly:docs:v3:doc:{type}:{id}` | Individual document data. Reads and autosaves are constant time O(1). | +| `veriworkly:docs:v3:index` | Metadata index (id, type, title, updatedAt, sync status, revision) used for fast library listing. | +| `veriworkly:docs:v2:active` | `TYPE:id` pointer to the currently active document (deliberately preserved across migrations). | +| `veriworkly:sync-outbox` | Pending background sync work items and retry queue. | +| `veriworkly:sync-telemetry` | Last attempt / success / error timestamps and error diagnostics. | +| `veriworkly:master-profile` | Local cached master profile facts record. | +| `veriworkly:workspace-settings` | Local workspace and editor preferences. | + +_(Note: `veriworkly:docs:v2:{type}` is legacy โ€” read once during automatic migration to v3, then cleaned up)._ Two custom DOM events (`veriworkly:docs-storage-updated` and `veriworkly:sync-outbox-updated`) let the autosave layer and the sync layer notify each other without diff --git a/apps/docs-platform/content/docs/contributing/coding-standards.mdx b/apps/docs-platform/content/docs/contributing/coding-standards.mdx index ff1eada2..6321b7f3 100644 --- a/apps/docs-platform/content/docs/contributing/coding-standards.mdx +++ b/apps/docs-platform/content/docs/contributing/coding-standards.mdx @@ -57,7 +57,7 @@ codebase actually follows. ## Accessibility -The project targets **WCAG 2.1โ€“2.2 AA**: +The project targets **WCAG 2.2 Level AA**: - Full keyboard operability and visible focus states. - Readable contrast in both themes. @@ -105,6 +105,7 @@ a welcome contribution. ```bash npm test -w @veriworkly/server # backend suite npm run test:contracts -w @veriworkly/studio # studio contract tests (fast) +npm run test:browser -w @veriworkly/studio # studio component browser tests npm run test:contracts -w @veriworkly/site # marketing-site contract tests npm test -w @veriworkly/portfolio # portfolio suite npm run test:parity -w @veriworkly/studio # preview/PDF parity (boots a browser, minutes) diff --git a/apps/docs-platform/content/docs/getting-started/local-setup.mdx b/apps/docs-platform/content/docs/getting-started/local-setup.mdx index b193778c..7dd72ac8 100644 --- a/apps/docs-platform/content/docs/getting-started/local-setup.mdx +++ b/apps/docs-platform/content/docs/getting-started/local-setup.mdx @@ -14,7 +14,7 @@ the platform. ## Prerequisites -- **Node.js** โ€” version 20 or higher. +- **Node.js** โ€” version 20.19.0 or higher (Node.js 22 supported). - **npm** โ€” the repository uses npm workspaces; other package managers are not configured. - **PostgreSQL** โ€” required for anything that touches the API. [Neon](https://neon.tech) works well as a managed option. @@ -44,11 +44,12 @@ the platform. cd veriworkly ``` - + `apps/portfolio/template-library` is a git submodule pointing at the private `VeriWorkly/portfolio-templates` repository. If you have access, initialise it with - `git submodule update --init --recursive`. If you do not, skip it โ€” every workspace except - `apps/portfolio` builds and runs normally without it. + `git submodule update --init --recursive`. If you do not have private repository access, + you can scaffold stand-in mock templates to build and run `apps/portfolio` locally by running: + `node scripts/mock-template-library.mjs`. diff --git a/apps/docs-platform/content/docs/getting-started/quick-start.mdx b/apps/docs-platform/content/docs/getting-started/quick-start.mdx index c69290d8..4f80b123 100644 --- a/apps/docs-platform/content/docs/getting-started/quick-start.mdx +++ b/apps/docs-platform/content/docs/getting-started/quick-start.mdx @@ -46,11 +46,11 @@ import { File, Folder, Files } from "fumadocs-ui/components/files"; - + `apps/portfolio/template-library` is a git submodule pointing at a **private** repository - (`VeriWorkly/portfolio-templates`). If you do not have access to it, `apps/portfolio` will not - build โ€” every other workspace is unaffected. See [Portfolio: - Templates](/docs/product/portfolio/templates). + (`VeriWorkly/portfolio-templates`). If you do not have SSH access, you can run `node + scripts/mock-template-library.mjs` to generate mock templates and build `apps/portfolio` locally. + See [Portfolio: Templates](/docs/product/portfolio/templates). --- diff --git a/apps/docs-platform/content/docs/index.mdx b/apps/docs-platform/content/docs/index.mdx index 035a6e29..df5a7d2d 100644 --- a/apps/docs-platform/content/docs/index.mdx +++ b/apps/docs-platform/content/docs/index.mdx @@ -42,9 +42,10 @@ before you sign in. Cloud sync is opt-in per account, and can be disabled per do }> - Six resume templates (Executive Clarity, Precision ATS, Modern Minimal, Timeline Focus, Corporate - Brief, Bold Impact) and two cover letter templates (Professional, VeriWorkly Special). All eight - are free โ€” there is no template lock. + Seven resume templates (Executive Clarity, Precision ATS, Modern Minimal, Timeline Focus, + Corporate Brief, Bold Impact, VeriWorkly Special) and five cover letter templates (Professional, + VeriWorkly Special, Minimalist, Executive, ATS Essential). All twelve are free โ€” there is no + template lock. }> diff --git a/apps/docs-platform/content/docs/operations/environment-variables.mdx b/apps/docs-platform/content/docs/operations/environment-variables.mdx index bcb16cd2..6ca8908c 100644 --- a/apps/docs-platform/content/docs/operations/environment-variables.mdx +++ b/apps/docs-platform/content/docs/operations/environment-variables.mdx @@ -29,18 +29,11 @@ Set per application in `apps/{app}/.env`, or shared via the root `.env`. | `ADMIN_EMAIL` | Yes | Server-only. Identifies the admin account; also unlocks the production-gated portfolio publish and checkout flows. Never exposed to the browser. | `admin@veriworkly.com` | | `ALLOWED_ORIGINS` | No | Permitted origins for the docs platform's API proxy route. | `https://api.veriworkly.com,https://veriworkly.com` | -### Studio-specific - -| Variable | Description | Default | -| :-------------------------- | :------------------------------------------------------------- | :---------------------- | -| `NEXT_PUBLIC_PORTFOLIO_URL` | Public URL of the portfolio builder, used for cross-app links. | `http://localhost:3004` | - ### Portfolio-specific -| Variable | Description | Default | -| :---------------------------- | :------------------------------------------------------------------------------------------------------------- | :---------------------- | -| `NEXT_PUBLIC_PORTFOLIO_URL` | Public URL of **Studio**, used for the "edit in Studio" links. | `http://localhost:3001` | -| `PORTFOLIO_REVALIDATE_SECRET` | Shared secret authorising the API's cache-revalidation callback after a publish. Must match the backend value. | โ€” | +| Variable | Description | Default | +| :---------------------------- | :------------------------------------------------------------------------------------------------------------- | :------ | +| `PORTFOLIO_REVALIDATE_SECRET` | Shared secret authorising the API's cache-revalidation callback after a publish. Must match the backend value. | โ€” | --- @@ -56,6 +49,7 @@ Set per application in `apps/{app}/.env`, or shared via the root `.env`. | `ALLOWED_ORIGINS` | Comma-separated CORS allowlist. Each entry must be a full origin, not a bare port. | Ports 3000โ€“3004 and 8080 on `http://localhost` | | `LOG_LEVEL` | `debug`, `info`, `warn`, or `error`. | `info` | | `AUDIT_LOG_RETENTION_DAYS` | How long `AuditLog` rows are kept. Production writes a row per 4xx/5xx, and the daily usage-metrics job is the only thing that prunes them. | `90` | +| `ANALYTICS_HASH_PEPPER` | Pepper used to compute non-reversible SHA-256 visitor identifier hashes for privacy-safe telemetry. | `veriworkly-analytics-pepper` | | `CLUSTERING_ENABLED` | Run one worker per CPU core via `throng`. | `true` in production, `false` otherwise | | `WEB_CONCURRENCY` / `SERVER_WORKERS` | Explicit worker count, overriding the CPU-core default. | CPU count | @@ -67,12 +61,15 @@ Set per application in `apps/{app}/.env`, or shared via the root `.env`. ### Database and cache -| Variable | Required | Description | Default | -| :------------------ | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- | -| `DATABASE_URL` | Yes | PostgreSQL connection string. | โ€” | -| `REDIS_URL` | Yes in practice | Redis connection string. Sessions, rate limiting, quotas, view buffers, and job locks all use it. | `redis://localhost:6379` | -| `DB_POOL_MAX_TOTAL` | No | Connection budget for the **whole cluster**, not per process. Set it to roughly 80% of the instance's real `max_connections`; the per-worker pool is derived by dividing this by the worker count. | `80` in production, `10` otherwise | -| `DB_POOL_MAX` | No | Explicit per-process override. Leave unset unless PgBouncer sits in front in transaction mode โ€” a stale value here is how you overcommit the database. | derived from `DB_POOL_MAX_TOTAL` | +| Variable | Required | Description | Default | +| :------------------------------ | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- | +| `DATABASE_URL` | Yes | PostgreSQL connection string. | โ€” | +| `REDIS_URL` | Yes in practice | Redis connection string. Sessions, rate limiting, quotas, view buffers, and job locks all use it. | `redis://localhost:6379` | +| `DB_POOL_MAX_TOTAL` | No | Connection budget for the **whole cluster**, not per process. Set it to roughly 80% of the instance's real `max_connections`; the per-worker pool is derived by dividing this by the worker count. | `80` in production, `10` otherwise | +| `DB_POOL_MAX` | No | Explicit per-process override. Leave unset unless PgBouncer sits in front in transaction mode โ€” a stale value here is how you overcommit the database. | derived from `DB_POOL_MAX_TOTAL` | +| `DB_POOL_IDLE_TIMEOUT_MS` | No | Database client idle timeout in milliseconds before connection is closed. | `15000` | +| `DB_POOL_CONNECTION_TIMEOUT_MS` | No | Maximum wait time in milliseconds to acquire a connection from the pool before timing out. | `30000` | +| `DB_POOL_STATEMENT_TIMEOUT_MS` | No | PostgreSQL server query execution statement timeout in milliseconds. | `30000` | With clustering on, each worker opens its own pool, so what must fit inside Postgres' diff --git a/apps/docs-platform/content/docs/product/cover-letter-builder.mdx b/apps/docs-platform/content/docs/product/cover-letter-builder.mdx index 69a3b6a6..13222027 100644 --- a/apps/docs-platform/content/docs/product/cover-letter-builder.mdx +++ b/apps/docs-platform/content/docs/product/cover-letter-builder.mdx @@ -64,6 +64,6 @@ switch (document.type) { ## Related - [Master Profile](/docs/product/master-profile) -- [Resume Templates](/docs/user-guides/resume-templates) โ€” includes both cover letter templates. +- [Resume Templates](/docs/user-guides/resume-templates) โ€” includes all five cover letter templates. - [Export Pipeline](/docs/architecture/pdf-generation) - [AI Writing Assistant](/docs/product/ai-writing-assistant) diff --git a/apps/docs-platform/content/docs/user-guides/creating-your-first-resume.mdx b/apps/docs-platform/content/docs/user-guides/creating-your-first-resume.mdx index a0f79bc9..9abb881c 100644 --- a/apps/docs-platform/content/docs/user-guides/creating-your-first-resume.mdx +++ b/apps/docs-platform/content/docs/user-guides/creating-your-first-resume.mdx @@ -67,20 +67,21 @@ specific role โ€” trim what is irrelevant without deleting anything. ## 4. Choose a template -Six resume templates, all free with no plan-based lock. Every one is single-column and ATS-safe by +Seven resume templates, all free with no plan-based lock. Every one is single-column and ATS-safe by design โ€” they differ in density, header treatment, and typographic breathing room, not in parseability. -| Template | Best for | -| :-------------------- | :-------------------------------------------------------------------------------- | -| **Executive Clarity** | The default. Balanced spacing and clear section segmentation. | -| **Precision ATS** | Longer resumes. High density, tuned for reliable machine parsing. | -| **Modern Minimal** | Letting the writing carry the page โ€” no rules, generous whitespace. | -| **Timeline Focus** | Long tenures. Dates sit in a fixed left gutter so a chronology scans in one pass. | -| **Corporate Brief** | Corporate applications. Split letterhead with accent-barred section headings. | -| **Bold Impact** | Career changes and senior pitches. Centred masthead over a heavy accent rule. | - -Switching templates never changes your data, so you can cycle through all six freely. See +| Template | Best for | +| :--------------------- | :-------------------------------------------------------------------------------- | +| **Executive Clarity** | The default. Balanced spacing and clear section segmentation. | +| **Precision ATS** | Longer resumes. High density, tuned for reliable machine parsing. | +| **Modern Minimal** | Letting the writing carry the page โ€” no rules, generous whitespace. | +| **Timeline Focus** | Long tenures. Dates sit in a fixed left gutter so a chronology scans in one pass. | +| **Corporate Brief** | Corporate applications. Split letterhead with accent-barred section headings. | +| **Bold Impact** | Career changes and senior pitches. Centred masthead over a heavy accent rule. | +| **VeriWorkly Special** | High-impact modern layout with distinct rules, badges, and balanced hierarchy. | + +Switching templates never changes your data, so you can cycle through all seven freely. See [Resume Templates](/docs/user-guides/resume-templates). --- diff --git a/apps/docs-platform/content/docs/user-guides/resume-templates.mdx b/apps/docs-platform/content/docs/user-guides/resume-templates.mdx index 5d25da49..b8a7a74f 100644 --- a/apps/docs-platform/content/docs/user-guides/resume-templates.mdx +++ b/apps/docs-platform/content/docs/user-guides/resume-templates.mdx @@ -1,6 +1,6 @@ --- title: Resume Templates -description: The document template system โ€” the eight built-in templates, the dual-engine model, and how to add your own. +description: The document template system โ€” the twelve built-in templates (7 resume, 5 cover letter), the dual-engine model, and how to add your own. --- import { LayoutTemplate, Mail } from "lucide-react"; @@ -15,7 +15,7 @@ library to compare layouts. ## Resume templates -All six are free. There is no template lock on resumes or cover letters. +All seven resume templates and five cover letter templates are free. There is no template lock on resumes or cover letters. }> @@ -43,9 +43,13 @@ All six are free. There is no template lock on resumes or cover letters. like an internal business brief. Default accent `#1d4ed8`. - }> - A centered, uppercase masthead over a heavy accent rule, with accent-underlined section headings. - For applications where the first impression has to carry weight. Default accent `#b91c1c`. +}> + A centered, uppercase masthead over a heavy accent rule, with accent-underlined section headings. + For applications where the first impression has to carry weight. Default accent `#b91c1c`. + + + }> + A modern high-impact layout with distinct accent rules, structured badges, and balanced typographic hierarchy. Default accent `#2563eb`. @@ -60,9 +64,24 @@ treatment, and typographic breathing room, not in parseability. `professional`. - }> - A branded two-column layout with an identity rail and numbered proof points. Template id - `veriworkly-special`. +}> + A branded layout with an identity rail, clean letterhead, and structured proof points. Template id + `veriworkly-special`. + + +}> + An ultra-clean, distraction-free typographic layout focusing entirely on clarity. Template id + `minimalist`. + + +}> + Sophisticated executive styling with premium letterhead and refined typography. Template id + `executive`. + + + }> + Streamlined, high-density format engineered specifically for automated parsing. Template id + `ats-essential`. @@ -91,16 +110,20 @@ visually aligned. See [Export Pipeline](/docs/architecture/pdf-generation). ## Source reference -| Template | Web engine | PDF engine | -| :------------------------------------ | :---------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | -| **Precision ATS** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/precision-ats/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/precision-ats/pdf.tsx) | -| **Executive Clarity** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/executive-clarity/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/executive-clarity/pdf.tsx) | -| **Modern Minimal** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/modern-minimal/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/modern-minimal/pdf.tsx) | -| **Timeline Focus** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/timeline-focus/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/timeline-focus/pdf.tsx) | -| **Corporate Brief** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/corporate-brief/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/corporate-brief/pdf.tsx) | -| **Bold Impact** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/bold-impact/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/bold-impact/pdf.tsx) | -| **Professional** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/professional/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/professional/pdf.tsx) | -| **VeriWorkly Special** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/veriworkly/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/veriworkly/pdf.tsx) | +| Template | Web engine | PDF engine | +| :------------------------------------ | :----------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | +| **Precision ATS** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/precision-ats/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/precision-ats/pdf.tsx) | +| **Executive Clarity** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/executive-clarity/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/executive-clarity/pdf.tsx) | +| **Modern Minimal** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/modern-minimal/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/modern-minimal/pdf.tsx) | +| **Timeline Focus** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/timeline-focus/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/timeline-focus/pdf.tsx) | +| **Corporate Brief** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/corporate-brief/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/corporate-brief/pdf.tsx) | +| **Bold Impact** | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/bold-impact/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/bold-impact/pdf.tsx) | +| **VeriWorkly Special** (resume) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/veriworkly-special/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/resume/veriworkly-special/pdf.tsx) | +| **Professional** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/professional/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/professional/pdf.tsx) | +| **VeriWorkly Special** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/veriworkly/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/veriworkly/pdf.tsx) | +| **Minimalist** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/minimalist/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/minimalist/pdf.tsx) | +| **Executive** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/executive/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/executive/pdf.tsx) | +| **ATS Essential** (cover letter) | [web.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/ats-essential/web.tsx) | [pdf.tsx](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/cover-letter/ats-essential/pdf.tsx) | The registry that wires templates into Studio is [`apps/studio/templates/index.ts`](https://github.com/VeriWorkly/veriworkly/blob/master/apps/studio/templates/index.ts). diff --git a/apps/docs-platform/package.json b/apps/docs-platform/package.json index f12b81ef..a9881f1e 100644 --- a/apps/docs-platform/package.json +++ b/apps/docs-platform/package.json @@ -1,6 +1,6 @@ { "name": "@veriworkly/docs-platform", - "version": "3.24.0", + "version": "3.24.1", "private": true, "scripts": { "generate:api": "node scripts/generate-api-docs.mjs", diff --git a/apps/docs-platform/public/.well-known/agent.json b/apps/docs-platform/public/.well-known/agent.json index 4f6c1e6c..c8f93f0c 100644 --- a/apps/docs-platform/public/.well-known/agent.json +++ b/apps/docs-platform/public/.well-known/agent.json @@ -26,7 +26,7 @@ { "name": "Interactive API Reference", "url": "https://docs.veriworkly.com/api-reference", - "description": "Fumadocs-powered interactive API explorer covering all 72 operations across 18 domain tags." + "description": "Fumadocs-powered interactive API explorer covering all 78 operations across 72 paths and 18 domain tags." }, { "name": "OpenAPI Specification", @@ -38,7 +38,7 @@ "capabilities": [ { "name": "API Reference Indexing", - "description": "Explore endpoint paths, request parameters, request payloads, and response envelopes for all 72 operations.", + "description": "Explore endpoint paths, request parameters, request payloads, and response envelopes for all 78 operations across 72 paths.", "endpoints": [ "/api/v1/health", "/api/v1/documents", diff --git a/apps/docs-platform/public/llms.txt b/apps/docs-platform/public/llms.txt index 068bba30..009aa522 100644 --- a/apps/docs-platform/public/llms.txt +++ b/apps/docs-platform/public/llms.txt @@ -37,7 +37,7 @@ Endpoints under `/api/v1/auth/*` are served by Better Auth and return **its** pa ### Pagination List endpoints use offset pagination. Page size defaults to 20 and is capped at 50, addressable as either `limit`/`offset` or `page`/`pageSize`; responses echo both alongside `total`, `totalPages`, `hasMore`, and a `pagination` block. `nextCursor` is always `null`. -## API Directory (72 operations, 152 schemas, 18 tags) +## API Directory (72 paths, 78 operations, 152 schemas, 18 tags) - **Health**: `GET /health`, `GET /health/ready` - **Authentication**: `POST /auth/email-otp/send-verification-otp`, `POST /auth/sign-in/email-otp`, `GET /auth/get-session`, `POST /auth/sign-out` - **API Keys**: `GET /api-keys`, `POST /api-keys`, `GET /api-keys/{id}`, `DELETE /api-keys/{id}`, `POST /api-keys/{id}/rotate`, `POST /api-keys/{id}/revoke` @@ -57,7 +57,7 @@ List endpoints use offset pagination. Page size defaults to 20 and is capped at - **Changelog**: `GET /changelog`, `GET /changelog/stats`, `GET /changelog/{id}` - **GitHub**: `GET /github/stats`, `GET /github/issues`, `POST /github/admin/sync` (admin only) -Not part of this reference: `/api/v1/admin/*` (admin console), `/api/v1/contact`, and `/api/v1/stats/events` are internal surfaces. +Not part of this reference: `/api/v1/admin/*` (admin console), `/api/v1/stats/admin/*` (admin telemetry dashboard), `/api/v1/contact` (contact form intake), and `/api/v1/stats/events` (telemetry ingress) are internal surfaces. ## Documentation Guides - **Introduction**: `https://docs.veriworkly.com/docs` @@ -87,6 +87,6 @@ Not part of this reference: `/api/v1/admin/*` (admin console), `/api/v1/contact` ## Key Product Facts - Documents are stored in the browser's `localStorage` first. Cloud sync requires an account and is opt-in per account and per document. - All six export formats (PDF, DOCX, HTML, Markdown, plain text, JSON) are generated client-side. There is no headless browser and no server-side render step. -- Six resume templates and two cover letter templates, all free. The four portfolio templates live in a private submodule; two of them are premium. +- Seven resume templates and five cover letter templates, all free. The four portfolio templates live in a private submodule; two of them are premium. - Payments run through Dodo Payments. VeriWorkly does not use Stripe. - The application code is MIT-licensed: `https://github.com/VeriWorkly/veriworkly` diff --git a/apps/docs-platform/scripts/generate-api-docs.mjs b/apps/docs-platform/scripts/generate-api-docs.mjs index bdc00c94..136bf084 100644 --- a/apps/docs-platform/scripts/generate-api-docs.mjs +++ b/apps/docs-platform/scripts/generate-api-docs.mjs @@ -31,6 +31,11 @@ const outputDir = path.join(appRoot, "content/api-reference"); const publicJsonPath = path.join(appRoot, "public/openapi.json"); const publicYamlPath = path.join(appRoot, "public/openapi.yaml"); +const repoRoot = path.join(appRoot, "..", ".."); +const satelliteJsonPaths = ["site", "studio", "portfolio", "blog-platform"].map((appName) => + path.join(repoRoot, "apps", appName, "public/openapi.json"), +); + /* ------------------------------------------------------------------ bundle */ const cache = new Map(); @@ -151,11 +156,16 @@ const bundledJson = await prettier.format(JSON.stringify(bundled), { fs.writeFileSync(bundlePath, bundledYaml, "utf8"); fs.writeFileSync(publicYamlPath, bundledYaml, "utf8"); fs.writeFileSync(publicJsonPath, bundledJson, "utf8"); +for (const targetPath of satelliteJsonPaths) { + if (fs.existsSync(path.dirname(targetPath))) { + fs.writeFileSync(targetPath, bundledJson, "utf8"); + } +} const pathCount = Object.keys(bundled.paths ?? {}).length; const schemaCount = Object.keys(bundled.components?.schemas ?? {}).length; console.log( - `[api-docs] bundled ${pathCount} paths and ${schemaCount} schemas -> openapi.yaml, public/openapi.{json,yaml}`, + `[api-docs] bundled ${pathCount} paths and ${schemaCount} schemas -> openapi.yaml, public/openapi.{json,yaml} across docs-platform, site, studio, portfolio, blog-platform`, ); /* ---------------------------------------------------------------- generate */ diff --git a/apps/portfolio/.env.example b/apps/portfolio/.env.example index f5b35789..2533e8b4 100644 --- a/apps/portfolio/.env.example +++ b/apps/portfolio/.env.example @@ -3,9 +3,7 @@ # ========================================================= SITE_URL=http://localhost:3004 - -# Studio App URL -NEXT_PUBLIC_PORTFOLIO_URL=http://localhost:3001 +NEXT_PUBLIC_APP_URL=http://localhost:3004 # Public API URL (used in browser/client-side code) # Must start with NEXT_PUBLIC_ for frontend exposure diff --git a/apps/portfolio/README.md b/apps/portfolio/README.md index c17ca17b..a51be267 100644 --- a/apps/portfolio/README.md +++ b/apps/portfolio/README.md @@ -30,7 +30,7 @@ A three-pane workspace (structure / content / live `
- Precision ATS + Precision ATS
Precision ATS
- Executive Clarity + Executive Clarity
Executive Clarity