This guide is the human-readable companion to openapi.yaml. The examples use raw Authorization header values. Do not prefix JWTs or MCP tokens with Bearer.
GET /api/v1/private/projects/{project_id}/documents/{document_id}/overview returns version_count, endpoint_count, latest_version (the existing public version DTO or null), published_branch_ids, has_reviewed_draft, raw_size_bytes, and nullable raw_line_count. Counts and latest-version selection cover the whole document; published branch IDs and reviewed-draft evidence only include active branches. The response contains no document body or storage keys. Markdown statistics are stored at publication; older versions populate the derived metadata once on first overview access.
GET /api/v1/private/projects/{project_id}/documents/{document_id}/mcp-readiness returns nullable last_read_at. It searches successful published-content reads for the selected document across all history, using only the current user's active, unexpired tokens with a matching read scope. Archived parents or absence of matching evidence return null. Both endpoints require document read permission and keep the HTTP 200 envelope contract.
Summary polling reads only the target and its permission context. On startup and every minute, the worker marks a pending summary as failed if it has no matching durable job and has remained pending for at least five minutes. The grace period protects an older instance's in-flight generation; existing queued or leased jobs are preserved. Manual regeneration performs the same recovery check before reusing pending state.
| Surface | Base |
|---|---|
| Public REST | /api/v1/open |
| Private REST | /api/v1/private |
| OpenAPI document | /api/v1/open/docs/openapi.yaml |
| MCP JSON-RPC | /api/v1/open/mcp |
Public auth normally starts with login. Anonymous registration is disabled by default and must be explicitly enabled with VDOC_AUTH_ALLOW_REGISTRATION=true only for a trusted disposable or pilot environment. Private REST calls use the JWT returned in the Vdoc envelope. MCP calls use an MCP token created through the private REST API.
API_BASE="${API_BASE:-http://127.0.0.1:8080}"
PASSWORD="sample-password-change-me"
# This request requires VDOC_AUTH_ALLOW_REGISTRATION=true on the backend.
REGISTER_RESPONSE=$(curl -sS "$API_BASE/api/v1/open/auth/register" \
-H 'Content-Type: application/json' \
-d '{"email":"docs-admin@example.test","name":"Docs Admin","password":"sample-password-change-me"}')
ADMIN_USER_ID=$(printf '%s' "$REGISTER_RESPONSE" | jq -r '.detail.user.id')
JWT=$(printf '%s' "$REGISTER_RESPONSE" | jq -r '.detail.token')REST handlers return HTTP 200 for both success and application errors. The semantic result is inside the JSON body.
| Field | Meaning |
|---|---|
code |
Semantic status code such as 200, 400, 401, 403, 404, 409, or 500. |
status |
Semantic status text such as OK, INVALID_ARGUMENT, UNAUTHENTICATED, PERMISSION_DENIED, NOT_FOUND, or INTERNAL. |
detail |
Optional result body. |
total |
Optional list count on list endpoints. |
Private project APIs use SuperAdmin plus project Reader, Writer, and Admin roles. Reader can query, Writer can upload draft and submit, and Admin can approve or reject.
Projects directly own typed Documents. document_type is 1 for OpenAPI, 2 for Markdown, and 3 for TXT. relative_path is the document path identity stored by Vdoc; the display name, filename, and containing folder are independent. Accessible project DTOs include team_name for navigation without granting access to the SuperAdmin-only Teams API.
List routes accept the PRD filters used by the workbench: document lists accept ?document_type=1|2|3, while draft and version lists accept ?branch_id={branch_id}. Draft detail/list responses include review_comment after request-changes, reject, or approve review actions.
Draft detail/list responses also include optional published_at, projected from the immutable version created from that draft. It is omitted before publication and is never inferred from updated_at. New drafts can optionally send source_version_id and source_branch_id with their normal content payload. The source must be a published version of the same project and document, and its branch must match. Read the version raw content first to seed the editor; source IDs record provenance and do not replace the submitted content.
GET /api/v1/private/projects/{project_id}/documents/{document_id}/activity requires document read permission. It returns workflow events newest first by (created_at, id), with opaque next_cursor pagination. limit defaults to 50 and is bounded to 1–200. The list envelope contains detail, has_more, and next_cursor; it does not perform an unbounded total count. Events include document/branch changes, draft creation/editing/submission/review, and publication. Each item projects only id, action, resource_id, optional branch_id, branch_name, version_name, review_action, and created_at. Internal audit metadata, review comments, tokens, IPs, and client details are excluded. Public-share capabilities cannot access this private endpoint.
GET /api/v1/private/projects/{project_id}/folders requires project read permission and returns a flat array of FolderDTO records in detail, plus total. Each record contains id, project_id, nullable parent_id, name, canonical project-local path, created_by, created_at, and updated_at. A null parent identifies a folder directly under the virtual project root. Empty folders are persisted, and folder nesting has no fixed depth limit.
POST to the same route requires Project Admin or SuperAdmin and an active project. Send {"name":"Guides","parent_id":null} to create a root folder, or supply a folder ID from the same project to create a child. Sibling names are unique without regard to case. Names are trimmed and must be single safe segments without separators, dot segments, or control characters. Teams and projects isolate their own trees; a folder from another project cannot be selected.
For document creation, send the display name, a separate file_name, and optional nullable folder_id. Omitting the folder, sending null, or sending an empty string selects the project root. The server derives relative_path. Filenames must be single safe segments; a missing extension becomes .yaml for OpenAPI, .md for Markdown, or .txt for TXT. OpenAPI also accepts .yml and .json. The legacy relative_path field remains supported and creates missing parent folders, but it cannot be combined with file_name or folder_id, even if it is empty or null.
Creation can optionally include the uploaded source as initial_content:
{
"name": "Installation guide",
"document_type": 3,
"file_name": "install.txt",
"folder_id": "selected-project-folder-id",
"initial_content": {
"content": "Install the application and configure its database.\n",
"version_name": "0.1.0",
"changelog": "Initial upload"
}
}initial_content.content must be a string. TXT accepts an empty string; Markdown and OpenAPI keep their normal validation. version_name defaults to 0.1.0. The document, default dev/test/prod branches, and first editable draft on dev are committed together; failure leaves none of those new records or automatically created parent folders. Uploading does not submit for review or publish. The response is a flat DocumentDTO with additional initial_draft_id and initial_branch_id. Without initial_content, creation produces the document and branches only.
Document DTOs include nullable folder_id, derived file_name, workflow_state (published, draft, or empty), has_draft, and available latest_version_id / latest_draft_id. A published document can also have an unpublished draft, so has_draft remains useful when workflow_state is published. These fields contain metadata only. The existing numeric status describes the active/archived lifecycle and must not be interpreted as publication state.
Document PATCH accepts independent name, file_name, and folder_id. An omitted filename or folder retains its current value; null or empty folder_id moves the document to the project root. Legacy relative_path cannot be combined with the new location fields. Renames and moves do not rewrite historical draft or version paths.
| Category | Purpose |
|---|---|
| Open | Health, login, opt-in registration, OpenAPI YAML, MCP JSON-RPC, and capability-authenticated public document shares. |
| Identity | Current JWT user identity. |
| System Users | SuperAdmin user lifecycle and user MCP token oversight. |
| Teams | Team lifecycle. |
| Projects | Project lifecycle and membership. |
| Documents | Project document lifecycle. |
| Document Folders | Project-local folder trees and empty folders. |
| Branches | Document branch lifecycle. |
| Drafts | Draft creation, update, submission, review, and promotion. |
| Versions | Published document versions and stored raw/normalized or stable content. |
| Endpoints | Parsed endpoint list and detail from published versions. |
| Diffs | Semantic version comparison and summaries. |
| MCP Tokens | User MCP token lifecycle. |
| AI | Built-in Admin AI provider, prompt, AI summary, and page chat APIs. |
| Document Shares | Admin-managed public capability links and anonymous published-content access. |
The scriptable smoke path covers register/login, create project/document, upload draft, submit, approve, query endpoint, compare diff, create MCP token, MCP tools/list, and MCP tools/call.
TEAM_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/teams" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d '{"name":"Docs Team","description":"API docs smoke team"}')
TEAM_ID=$(printf '%s' "$TEAM_RESPONSE" | jq -r '.detail.id')
PROJECT_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d "{\"team_id\":\"$TEAM_ID\",\"name\":\"Docs Project\",\"description\":\"API docs smoke project\",\"admin_user_id\":\"$ADMIN_USER_ID\"}")
PROJECT_ID=$(printf '%s' "$PROJECT_RESPONSE" | jq -r '.detail.id')
DOCUMENT_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d '{"name":"petstore","document_type":1,"relative_path":"apis/petstore.yaml","description":"Docs sample document"}')
DOCUMENT_ID=$(printf '%s' "$DOCUMENT_RESPONSE" | jq -r '.detail.id')
BRANCH_ID=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/branches" \
-H "Authorization: $JWT" | jq -r '.detail[] | select(.name=="dev") | .id')Document creation creates dev, test, and protected prod branches. Admins can create and rename custom branches such as main, release, feature/checkout, or release/2026; the feature/ prefix is optional. The exact names dev, test, and prod retain environment kind, and other valid names use feature kind. Names are trimmed, must be non-empty valid text, and cannot contain backslashes, control characters, empty slash-separated segments, or ./.. segments. Names stay case-sensitive and unique within the document, including archived branches. Creating a branch creates no draft or published version.
Document lifecycle state is not patchable. PATCH /api/v1/private/projects/{project_id}/documents/{document_id} updates document metadata only; omit status (or send the unchanged active value for legacy clients). Archive a document only through POST /api/v1/private/projects/{project_id}/documents/{document_id}/archive. A rejected status change does not partially update the other fields.
Create a draft by posting OpenAPI 3.0 or 3.1 content as schema_content. Markdown and TXT documents use the same private REST draft routes and may post source text as schema_content or content. MCP text-document draft tools accept content and retain the legacy markdown_content alias. content_kind accepts raw or normalized for OpenAPI content and raw or stable for Markdown/TXT content. TXT preserves source bytes and line endings, permits an empty string, and rejects missing or null content.
Draft responses include an opaque revision. Every REST draft PATCH and MCP update_api_version_draft / update_doc_draft call requires expected_revision from the draft snapshot the edits were based on. A missing revision returns INVALID_ARGUMENT; a stale revision returns FAILED_PRECONDITION without changing the draft. Reload and reconcile local edits before retrying. REST draft content responses include detail.draft alongside content and hash, all from one snapshot; editors must use this nested draft's metadata and revision. Deploy the backend and Admin together, and update MCP clients to pass the new required field.
Submitted draft snapshots also include review_revision. Approval, request-changes, and rejection require expected_review_revision from the exact detail.draft returned with the content and diff the reviewer inspected. The review revision binds content, submission round, and the current branch latest used by the preview. Missing values return INVALID_ARGUMENT; changed content, resubmission (even with identical content), or a newer branch publication return FAILED_PRECONDITION. Reload the content and diff, preserve the review note, and make a new review decision. The server also rechecks the draft and branch baseline within the publish transaction. Published drafts retain their historical review baseline. Backend and Admin must be upgraded together.
DRAFT_ONE_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/drafts" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d "{\"branch_id\":\"$BRANCH_ID\",\"version_name\":\"1.0.0\",\"schema_content\":$SCHEMA_V1}")
DRAFT_ONE_ID=$(printf '%s' "$DRAFT_ONE_RESPONSE" | jq -r '.detail.id')
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/drafts/$DRAFT_ONE_ID/content/raw" -H "Authorization: $JWT"
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/drafts/$DRAFT_ONE_ID/content/normalized" -H "Authorization: $JWT"
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/drafts/$DRAFT_ONE_ID/submit" -X POST -H "Authorization: $JWT"
REVIEW_SNAPSHOT=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/drafts/$DRAFT_ONE_ID/content/raw" -H "Authorization: $JWT")
printf '%s' "$REVIEW_SNAPSHOT" | jq '{content: .detail.content, diff: .detail.draft.diff_preview}'
# Inspect the content and diff before approving this snapshot.
REVIEW_BODY=$(printf '%s' "$REVIEW_SNAPSHOT" | jq -c '{expected_review_revision: .detail.draft.review_revision}')
VERSION_ONE_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/drafts/$DRAFT_ONE_ID/approve" \
-X POST -H "Authorization: $JWT" -H 'Content-Type: application/json' -d "$REVIEW_BODY")
VERSION_ONE_ID=$(printf '%s' "$VERSION_ONE_RESPONSE" | jq -r '.detail.id')Other review and promotion calls use:
POST /api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/request-changes
POST /api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/reject
POST /api/v1/private/projects/{project_id}/documents/{document_id}/drafts/promote
Review actions accept an optional JSON comment, trim it, persist it on the draft, return it as review_comment, and include it in review audit metadata without placing it in the published version. Unknown review fields are rejected.
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/versions/$VERSION_ONE_ID/content/raw" -H "Authorization: $JWT"
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/versions/$VERSION_ONE_ID/content/normalized" -H "Authorization: $JWT"
ENDPOINTS_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/versions/$VERSION_ONE_ID/endpoints?path=/pets" -H "Authorization: $JWT")
ENDPOINT_ID=$(printf '%s' "$ENDPOINTS_RESPONSE" | jq -r '.detail[0].id')
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/versions/$VERSION_ONE_ID/endpoints/$ENDPOINT_ID" -H "Authorization: $JWT"
DIFF_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/diffs" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d "{\"from_version_id\":\"$VERSION_ONE_ID\",\"to_version_id\":\"$VERSION_TWO_ID\"}")
DIFF_ID=$(printf '%s' "$DIFF_RESPONSE" | jq -r '.detail.id')
curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/diffs/$DIFF_ID/summary" -H "Authorization: $JWT"OpenAPI summaries use added_endpoints, removed_endpoints, modified_endpoints, and breaking_changes. Markdown summaries set document_format: 3 and use added_lines, removed_lines, modified_lines, and modified_blocks; endpoint counts remain zero. TXT uses document_format: 4 with line comparisons that preserve newline differences, and has no API endpoint semantics.
Endpoint details resolve local references and apply operation-level parameter overrides by name + in; duplicate identities at the same level are rejected. OpenAPI 3.1 Schema $ref siblings remain active alongside the referenced schema (represented with allOf), while 3.0 Reference Object siblings remain ignored. Used security scheme definitions are included in normalized_operation.securitySchemes; unused definitions are omitted. Changes to these definitions emit security_changed items at securitySchemes with the previous and current definitions, using the existing warning severity for security changes. This is available through both REST endpoint detail and MCP get_endpoint_detail; tool arguments are unchanged.
On upgraded backends, legacy published endpoint indexes are refreshed from hash-verified immutable source content when read. Cached OpenAPI version comparisons are recalculated and persisted on first access, retaining the version, endpoint, and diff IDs. The original published document is unchanged; object storage must remain available for this refresh.
Calling Compare requires an active Project and Document, and from_version_id must differ from to_version_id. Any two immutable published versions within that Document may be compared across branches, including archived Branches. Branch archive blocks new draft and AI work in that Branch but permits version comparisons. Project or Document archive blocks Compare requests; previously stored Diff records and summaries remain readable after their Project, Document, or Branch is archived.
MCP_TOKEN_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/mcp-tokens" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d '{"name":"docs-agent","scopes":[1,2]}')
MCP_TOKEN=$(printf '%s' "$MCP_TOKEN_RESPONSE" | jq -r '.detail.token')
MCP_TOKEN_ID=$(printf '%s' "$MCP_TOKEN_RESPONSE" | jq -r '.detail.id')
curl -sS "$API_BASE/api/v1/open/mcp" \
-H 'Content-Type: application/json' \
-H "Authorization: $MCP_TOKEN" \
-d '{"jsonrpc":"2.0","id":"tools-list","method":"tools/list"}'
curl -sS "$API_BASE/api/v1/open/mcp" \
-H 'Content-Type: application/json' \
-H "Authorization: $MCP_TOKEN" \
-d "{\"jsonrpc\":\"2.0\",\"id\":\"endpoint-detail\",\"method\":\"tools/call\",\"params\":{\"name\":\"get_endpoint_detail\",\"arguments\":{\"project_id\":\"$PROJECT_ID\",\"document_id\":\"$DOCUMENT_ID\",\"version_id\":\"$VERSION_ONE_ID\",\"endpoint_id\":\"$ENDPOINT_ID\"}}}"
curl -sS "$API_BASE/api/v1/private/mcp-usage?token_id=$MCP_TOKEN_ID&limit=20" \
-H "Authorization: $JWT"The token value in .detail.token is returned on creation and may be revealed again by the token owner with GET /api/v1/private/mcp-tokens/{token_id} while the token remains active. List, revoked, and expired token responses stay redacted. Storage uses token_hash for authentication and encrypted token_ciphertext for owner reveal; neither storage field is exposed by the API.
GET /api/v1/private/mcp-usage returns newest-first sanitized mcp.tool_call evidence for the authenticated user's tokens. token_id selects one owned token; SuperAdmins may specify another user's token, but an omitted token_id always means the current user's own tokens. The response is capped at 200 records and retains only the tool, adapter, result/reason, token ID, and canonical project/document/branch/draft/version/endpoint/diff IDs. Entity IDs are recorded only after a successful tool call, so a failed request cannot turn caller-supplied IDs into entity evidence. Successful published-content reads use evidence_kind=published_content_read. Raw MCP secrets, request arguments, schema/Markdown content, IP addresses, and User-Agent values are not returned.
Current MCP tools include list_projects, list_documents, list_document_branches, list_api_endpoints, list_api_versions, list_doc_versions, get_latest_schema, get_schema_version, get_endpoint_detail, compare_api_versions, get_change_summary, create_api_version_draft, update_api_version_draft, submit_api_version_draft, get_api_version_draft, get_latest_doc, get_doc_version, compare_doc_versions, create_doc_draft, update_doc_draft, submit_doc_draft, and get_doc_draft. Direct publish tools are intentionally not exposed in v0.1. list_api_versions is retained for API-document clients; Markdown agents should use the discoverable list_doc_versions tool with a doc:read scope.
MCP discovery in the current source version: list_document_branches(project_id, document_id) returns branch IDs/names/default/protection/status even before publication and requires the target document type's read scope. list_api_endpoints(project_id, document_id, version_id, method?, path?) returns endpoint summaries with IDs; method is case-insensitive and path matches the exact OpenAPI template. Use a returned ID with get_endpoint_detail. Older deployments may lack these tools; inspect tools/list before use.
SuperAdmins alone can read, update, and test the system OpenAI-compatible provider and system prompts at /api/v1/private/ai/*. Project Admins and SuperAdmins can read, update, and test project provider and prompt configuration under /api/v1/private/projects/{project_id}/ai/*; Reader and Writer roles are denied those configuration APIs even though they may use page summaries and Chat where document permissions allow. Ordinary provider responses expose api_key_set and api_key_last4 only. Explicit key reveal uses POST /api/v1/private/ai/provider/key/reveal (SuperAdmin) or POST /api/v1/private/projects/{project_id}/ai/provider/key/reveal (Project Admin/SuperAdmin, active project), returns detail: {"api_key":"…"} with Cache-Control: no-store, and records a secret-free ai.provider.key.reveal audit before returning the key. Project reveal never exposes the system fallback key. Admin keeps the saved key masked until the eye button is clicked; hiding it clears the revealed value and merely viewing it never sends a replacement key on save or model discovery. AI summary and chat outputs are AI-generated helper text and cannot approve, request changes, reject, publish, or modify drafts or versions.
Provider payloads accept tuning fields alongside name, base_url, model, api_mode, api_key, and enabled. temperature defaults to 0.2 and accepts 0 through 2. timeout_ms defaults to 30000 and accepts 1000 through 120000. max_output_tokens defaults to 1000 and accepts 1 through 32000. Project provider endpoints use the same request and response shape as the system provider.
Admin places the key before the model field, defaults new configurations to Responses, and uses a 0–2 temperature slider with reset to 0.2. Existing API modes and explicit zero temperature are preserved. Model discovery and inference share one endpoint rule: a host root gets /v1, a Base URL ending in /v1 does not get a second copy, and a gateway prefix such as /gateway/v1 is preserved. Model-list availability does not imply support for both inference modes; select the API mode supported by the provider.
The provider timeout controls the complete AI HTTP request. The default backend server.write_timeout is 180s to allow a 120-second provider call and request/response processing. Existing deployments with an explicit shorter write timeout should update it (or set VDOC_SERVER_WRITE_TIMEOUT=180s) before using longer provider timeouts; external proxy/client timeouts must also allow the request. Admin connects to the configured backend origin directly. The current MCP adapter defaults VDOC_MCP_TIMEOUT_MS to 180000 and accepts 1 through 180000. Upgrade older adapters before setting this value, and allow at least the same duration in the agent host's tool timeout when waiting for long AI-assisted draft submissions.
Token-limit truncation and other explicit incomplete provider responses fail the AI generation instead of saving a successful partial summary. Draft submission/publication retains its business result when an automatic AI summary fails; the AI panel reports the failed summary so it can be retried after changing the output limit. Context previews disclose truncation and carry version/branch identity; semantic-diff prompts include original change flags and old/new values.
Provider tests accept an optional request body. Omitting the body tests the saved effective configuration; for a project without an enabled override, this deliberately tests the enabled system-provider fallback. Prompt update bodies contain system_prompt, user_prompt_template, and enabled; the path is the single source of truth for prompt_key. Both prompt strings must be non-blank, every user_prompt_template must contain the literal {{context}}, and page_chat must additionally contain {{message}}.
Provider model discovery uses POST /api/v1/private/ai/provider/models (SuperAdmin) or POST /api/v1/private/projects/{project_id}/ai/provider/models (Project Admin/SuperAdmin, active project). An optional body supplies base_url, optional api_key, and optional timeout_ms; no model name is required. With no body, discovery uses the saved provider, including disabled configuration; a missing project provider can use the enabled system fallback. With a body, the saved key is reused only for an unchanged normalized URL. Changing the URL requires a supplied key. Credentials remain on the backend, and discovery neither saves configuration nor makes an inference request.
The backend requests the OpenAI-compatible /v1/models endpoint (without duplicating a trailing /v1), uses the existing HTTPS/DNS/redirect protections and bounded timeout, and accepts at most 4 MiB / 4096 model entries. Success returns detail: {"models":["model-a","model-b"]} with sorted unique IDs; empty lists are valid. Provider errors and unsupported model-list endpoints leave manual model input available. Opening the model chooser fetches the current connection; changed URL/key inputs invalidate its list and late responses. Discovery is configuration access, so Reader/Writer roles and archived project configuration cannot issue these remote requests.
curl -sS "$API_BASE/api/v1/private/ai/provider" \
-X PUT \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d '{"name":"docs-ai","base_url":"https://api.openai.example","model":"gpt-4.1-mini","api_mode":"chat_completions","api_key":"sk-change-me","enabled":true,"temperature":0.2,"timeout_ms":30000,"max_output_tokens":1000}'Submitting a draft atomically saves its submitted state, pending AI summary, and background job. Approving a draft atomically publishes the version and its summary job. Provider calls run after the transaction in a background worker, so submit and approve responses do not wait for AI. OpenAPI and Markdown draft submit and approve paths both follow this rule. The summary attempt is helper work, not part of the approval decision.
Manual ai-summary/regenerate also queues work and returns pending; repeating it while pending reuses that generation. Poll the matching GET endpoint every two seconds until a terminal status, and stop polling on navigation or read errors. The latest in-flight generation is visible as pending. Skipped and failed automatic summaries are saved as non-blocking skipped or failed records. Missing providers and disabled prompts are stored as skipped; provider call errors are stored as failed. A completion is rejected if its target, permissions, provider, or prompt changed while the provider call was running. These records are visible through the same ai-summary read endpoints and do not roll back submit or publish.
AI chat sends a bounded window of the current session history. Each session uses a persisted generation token so an older provider response cannot overwrite a newer request, including when requests are handled by different Vdoc instances. The chat-session collection supports GET with required document_id, context_type, and context_id query parameters so clients can recover all sessions for the current page context, newest-updated first.
Archiving is a read-only boundary for AI history. Project provider and prompt configuration remains readable to Project Admins and SuperAdmins after Project archive, while provider update/test and prompt update are blocked. Reader and Writer roles still cannot read that configuration. Existing Summary records, Chat sessions, and Chat messages remain readable after Project, Document, or target Branch archive. Manual regeneration, new Chat session creation, and new Chat messages require an active Project, Document, and target Branch. Manual Summary regeneration additionally requires Project Admin or SuperAdmin permission; readable project members may use Chat while the context is active. A provider completion that returns after the context or relevant configuration became stale is discarded.
This produces the following archive boundary across the private APIs:
| Resource after parent archive | Read/list | New work | Remaining lifecycle action |
|---|---|---|---|
| Draft | Allowed | Mutation blocked after Project, Document, or target Branch archive | None |
| Version, stored Diff | Allowed | Compare blocked after Project or Document archive; allowed after Branch archive | None |
| AI provider/prompts | Project Admin/SuperAdmin only | Update and provider test blocked | None |
| AI Summary/Chat | Allowed | Regenerate, create session, and send blocked | None |
| Document share | List allowed | Create and reveal blocked | Revoke allowed |
AI audit metadata includes token usage fields when the provider returns them: prompt_tokens, completion_tokens, and total_tokens. API keys, JWTs, MCP tokens, and Authorization headers are not stored in AI audit metadata. Provider test calls, manual summary regeneration, automatic summaries, and chat calls are audited with status and non-secret context.
GET /api/v1/private/ai/provider
PUT /api/v1/private/ai/provider
POST /api/v1/private/ai/provider/test
POST /api/v1/private/ai/provider/models
POST /api/v1/private/ai/provider/key/reveal
GET /api/v1/private/projects/{project_id}/ai/provider
PUT /api/v1/private/projects/{project_id}/ai/provider
POST /api/v1/private/projects/{project_id}/ai/provider/test
POST /api/v1/private/projects/{project_id}/ai/provider/models
POST /api/v1/private/projects/{project_id}/ai/provider/key/reveal
GET /api/v1/private/ai/prompts
PUT /api/v1/private/ai/prompts/{prompt_key}
GET /api/v1/private/projects/{project_id}/ai/prompts
PUT /api/v1/private/projects/{project_id}/ai/prompts/{prompt_key}
GET /api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/ai-summary
POST /api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/ai-summary/regenerate
GET /api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/ai-summary
POST /api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/ai-summary/regenerate
GET /api/v1/private/projects/{project_id}/documents/{document_id}/diffs/{diff_id}/ai-summary
POST /api/v1/private/projects/{project_id}/documents/{document_id}/diffs/{diff_id}/ai-summary/regenerate
GET /api/v1/private/projects/{project_id}/ai/chat-sessions?document_id={document_id}&context_type={document|draft|version|diff}&context_id={context_id}
POST /api/v1/private/projects/{project_id}/ai/chat-sessions
GET /api/v1/private/projects/{project_id}/ai/chat-sessions/{session_id}
POST /api/v1/private/projects/{project_id}/ai/chat-sessions/{session_id}/messages
Document-wide chat uses context_type: "document" and requires context_id to equal document_id. It reads only sources belonging to that authorized document: published revisions and saved drafts across its branches, including archived branch history. Reader, Writer and Admin project access remains required; public shares and MCP do not gain this access. The Admin workspace keeps this document conversation while its independently scoped revision summary follows the reading selection. Existing revision/diff sessions remain readable through their original context filters.
For each question the server ranks all source metadata by explicit branch/revision matches and recency, diversifying the latest published and draft source per branch. It sends at most 16 bodies, 48,000 body characters and a 12,000-character catalog. Long bodies provide a question-matching excerpt; omissions and source identities are explicit in the prompt. This is bounded retrieval, not a guarantee that every revision's full text fits one request. Name a branch, revision or ID for precise historical questions. Source attribution instructions distinguish reviewed versions from draft state. The server rechecks the entire document source fingerprint, project permission, effective provider/prompt and conversation generation in the completion transaction; concurrent source changes invalidate the answer without saving partial conversation messages.
Migration 012_document_ai_chat.sql extends only chat-session context types, preserving existing sessions and keeping summary/job targets restricted to draft/version/diff. Deploy the backend and Admin together so the workspace can create document sessions.
Only Project Admins and SuperAdmins can manage public links. A branch must be active and already contain at least one published version before a link can be created. version_scope is 1 for the moving latest published version, 2 for all published versions on the branch, or 3 for one fixed published version. Scope 3 requires a nonempty version_id belonging to the same project, document, and branch; scopes 1 and 2 must omit version_id. Drafts cannot be shared. The private create/list/reveal/revoke response includes version_id for fixed-version links. expiry_preset accepts 1_month, 3_months, 6_months, 1_year, or permanent; Admin selects 3_months by default. password is optional and, when present, must be 12–72 UTF-8 bytes with no leading or trailing Unicode whitespace.
For a fixed-version link, public metadata's current_version always identifies that version, including after newer versions are published. Content and download routes accept only its version_id; other IDs return the same unavailable response. The public versions-list route remains available only to scope 2, so fixed-version links expose no other version metadata. Existing scope 1 and 2 links keep their original behavior.
After the parent Project, Document, or Branch is archived, Admins can still list retained share history and revoke an active link. Creating a link and revealing its capability are blocked. Anonymous public access remains unavailable whenever a parent is inactive and deliberately does not reveal which lifecycle check failed.
SHARE_RESPONSE=$(curl -sS "$API_BASE/api/v1/private/projects/$PROJECT_ID/documents/$DOCUMENT_ID/shares" \
-H 'Content-Type: application/json' \
-H "Authorization: $JWT" \
-d "{\"branch_id\":\"$BRANCH_ID\",\"version_scope\":2,\"expiry_preset\":\"1_month\",\"password\":\"sample share password\"}")
SHARE_ID=$(printf '%s' "$SHARE_RESPONSE" | jq -r '.detail.share.id')
SHARE_SECRET=$(printf '%s' "$SHARE_RESPONSE" | jq -r '.detail.secret')The complete browser URL is built as /share/{share_id}#{secret}. The fragment must be removed from the current history entry before any network request. Anonymous API calls send Authorization: VdocShare {secret}, omit account cookies/JWT, and receive Cache-Control: no-store, Referrer-Policy: no-referrer, and X-Robots-Tag: noindex protections.
Password-protected shares exchange the password for a 15-minute, share-bound proof:
POST /api/v1/open/document-shares/{share_id}/unlock
GET /api/v1/open/document-shares/{share_id}
GET /api/v1/open/document-shares/{share_id}/versions
GET /api/v1/open/document-shares/{share_id}/versions/{version_id}/content
GET /api/v1/open/document-shares/{share_id}/versions/{version_id}/download
Send the proof as X-Vdoc-Share-Unlock. Invalid capabilities, passwords, proofs, revoked/expired links, and inactive parent resources all return the same public unavailable response. Downloads always pass through Vdoc authorization; object storage remains private. Markdown viewers must disable raw HTML, remote images, and unsafe links, while OpenAPI content is rendered only as escaped read-only text.
All active signed-in users can read GET /api/v1/private/system/version. Its detail contains version, git_commit, build_time, and channel from the running backend binary's build metadata. A strict vX.Y.Z-beta.N build has channel beta, a strict vX.Y.Z build has channel release, and untagged, dirty, RC, or unknown builds have channel development. Reading this endpoint does not check for updates.
Call GET /api/v1/private/system/updates only when the user requests an update check. This read-only endpoint queries the fixed Vdoc release repository; it never installs or restarts anything. Beta deployments select only published beta prereleases; stable deployments select only published stable releases. Drafts, RCs, invalid tags, and inconsistent GitHub prerelease flags are ignored. Versions are compared numerically (beta.10 is newer than beta.9), independently of publication order. Successful results are cached per channel for two minutes; requests are bounded to ten seconds, ten pages of 100 releases, and 4 MiB per page. An incomplete or failed check is an error.
The result's detail contains current_version, channel, latest_version, update_available, release_url, and published_at. The last three release metadata strings (latest_version, release_url, published_at) are null if no matching release exists; update_available is then false. Release links always point to https://github.com/ChnMig/Vdoc-site/releases/tag/<version>.
Development builds return FAILED_PRECONDITION when checking updates. Network, GitHub rate-limit, and malformed-response failures return UNAVAILABLE; deadlines return DEADLINE_EXCEEDED, and canceled requests return CANCELLED. The HTTP status remains 200 under the standard Vdoc envelope contract. A failed check must not be presented as “up to date.” Operators still update their Docker Compose deployment manually.
| Category | Methods | Path | Auth |
|---|---|---|---|
| Open | GET |
/api/v1/open/health |
Public |
| Open | GET |
/api/v1/open/auth/config |
Public |
| Open | POST |
/api/v1/open/auth/register |
Public |
| Open | POST |
/api/v1/open/auth/login |
Public |
| Open | GET |
/api/v1/open/docs/openapi.yaml |
Public |
| Open | POST |
/api/v1/open/mcp |
MCP token |
| Document Shares | GET |
/api/v1/open/document-shares/{share_id} |
Share capability |
| Document Shares | POST |
/api/v1/open/document-shares/{share_id}/unlock |
Share capability |
| Document Shares | GET |
/api/v1/open/document-shares/{share_id}/versions |
Share capability/proof |
| Document Shares | GET |
/api/v1/open/document-shares/{share_id}/versions/{version_id}/content |
Share capability/proof |
| Document Shares | GET |
/api/v1/open/document-shares/{share_id}/versions/{version_id}/download |
Share capability/proof |
| Identity | GET |
/api/v1/private/identity/me |
JWT |
| Identity | POST |
/api/v1/private/identity/me/password |
JWT; current password |
| System | GET |
/api/v1/private/system/version |
JWT; all active signed-in users |
| System | GET |
/api/v1/private/system/updates |
JWT; all active signed-in users |
| System Users | GET, POST |
/api/v1/private/system/users |
JWT |
| System Users | PATCH |
/api/v1/private/system/users/{user_id} |
JWT |
| System Users | GET |
/api/v1/private/system/users/{user_id}/mcp-tokens |
JWT |
| System Users | POST |
/api/v1/private/system/users/{user_id}/mcp-tokens/{token_id}/revoke |
JWT |
| Teams | GET, POST |
/api/v1/private/teams |
JWT; SuperAdmin |
| Teams | GET, PATCH |
/api/v1/private/teams/{team_id} |
JWT; SuperAdmin |
| Teams | POST |
/api/v1/private/teams/{team_id}/archive |
JWT; SuperAdmin |
| Projects | GET, POST |
/api/v1/private/projects |
JWT |
| Projects | GET, PATCH |
/api/v1/private/projects/{project_id} |
JWT |
| Projects | POST |
/api/v1/private/projects/{project_id}/archive |
JWT |
| Projects | GET, POST |
/api/v1/private/projects/{project_id}/members |
JWT |
| Projects | GET |
/api/v1/private/projects/{project_id}/member-candidates |
JWT; Project Admin or SuperAdmin |
| Projects | DELETE |
/api/v1/private/projects/{project_id}/members/{user_id} |
JWT |
| Projects | PATCH |
/api/v1/private/projects/{project_id}/members/{user_id}/role |
JWT |
| Document Folders | GET, POST |
/api/v1/private/projects/{project_id}/folders |
JWT; GET requires project read permission, POST requires Project Admin/SuperAdmin |
| Documents | GET, POST |
/api/v1/private/projects/{project_id}/documents |
JWT |
| Documents | GET, PATCH |
/api/v1/private/projects/{project_id}/documents/{document_id} |
JWT |
| Documents | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/archive |
JWT |
| Document Activity | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/activity |
JWT; document read permission |
| Document Shares | GET, POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/shares |
JWT (Project Admin/SuperAdmin) |
| Document Shares | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/shares/{share_id}/reveal |
JWT (Project Admin/SuperAdmin) |
| Document Shares | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/shares/{share_id}/revoke |
JWT (Project Admin/SuperAdmin) |
| Branches | GET, POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/branches |
JWT |
| Branches | GET, PATCH |
/api/v1/private/projects/{project_id}/documents/{document_id}/branches/{branch_id} |
JWT |
| Branches | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/branches/{branch_id}/archive |
JWT |
| Drafts | GET, POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts |
JWT |
| Drafts | GET, PATCH |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id} |
JWT |
| Drafts | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/content/{content_kind} |
JWT |
| Drafts | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/submit |
JWT |
| Drafts | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/approve |
JWT |
| Drafts | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/request-changes |
JWT |
| Drafts | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/reject |
JWT |
| Drafts | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/promote |
JWT |
| Versions | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions |
JWT |
| Versions | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id} |
JWT |
| Versions | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/content/{content_kind} |
JWT |
| Endpoints | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/endpoints |
JWT |
| Endpoints | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/endpoints/{endpoint_id} |
JWT |
| Diffs | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/diffs |
JWT |
| Diffs | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/diffs |
JWT |
| Diffs | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/diffs/{diff_id} |
JWT |
| Diffs | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/diffs/{diff_id}/summary |
JWT |
| Audit Logs | GET |
/api/v1/private/audit-logs |
JWT; super admin or scoped project admin |
| AI | GET, PUT |
/api/v1/private/ai/provider |
JWT; SuperAdmin |
| AI | POST |
/api/v1/private/ai/provider/test |
JWT; SuperAdmin |
| AI | POST |
/api/v1/private/ai/provider/models |
JWT; SuperAdmin |
| AI | POST |
/api/v1/private/ai/provider/key/reveal |
JWT; SuperAdmin; explicit audited no-store key reveal |
| AI | GET, PUT |
/api/v1/private/projects/{project_id}/ai/provider |
JWT; Project Admin or SuperAdmin |
| AI | POST |
/api/v1/private/projects/{project_id}/ai/provider/test |
JWT; Project Admin or SuperAdmin |
| AI | POST |
/api/v1/private/projects/{project_id}/ai/provider/models |
JWT; Project Admin or SuperAdmin; active project |
| AI | POST |
/api/v1/private/projects/{project_id}/ai/provider/key/reveal |
JWT; Project Admin or SuperAdmin; active project; own override only |
| AI | GET |
/api/v1/private/ai/prompts |
JWT; SuperAdmin |
| AI | PUT |
/api/v1/private/ai/prompts/{prompt_key} |
JWT; SuperAdmin |
| AI | GET |
/api/v1/private/projects/{project_id}/ai/prompts |
JWT; Project Admin or SuperAdmin |
| AI | PUT |
/api/v1/private/projects/{project_id}/ai/prompts/{prompt_key} |
JWT; Project Admin or SuperAdmin |
| AI | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/ai-summary |
JWT |
| AI | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/drafts/{draft_id}/ai-summary/regenerate |
JWT |
| AI | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/ai-summary |
JWT |
| AI | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/versions/{version_id}/ai-summary/regenerate |
JWT |
| AI | GET |
/api/v1/private/projects/{project_id}/documents/{document_id}/diffs/{diff_id}/ai-summary |
JWT |
| AI | POST |
/api/v1/private/projects/{project_id}/documents/{document_id}/diffs/{diff_id}/ai-summary/regenerate |
JWT |
| AI | GET, POST |
/api/v1/private/projects/{project_id}/ai/chat-sessions |
JWT |
| AI | GET |
/api/v1/private/projects/{project_id}/ai/chat-sessions/{session_id} |
JWT |
| AI | POST |
/api/v1/private/projects/{project_id}/ai/chat-sessions/{session_id}/messages |
JWT |
| MCP Tokens | GET, POST |
/api/v1/private/mcp-tokens |
JWT |
| MCP Tokens | GET |
/api/v1/private/mcp-usage |
JWT; owner-scoped unless SuperAdmin supplies token_id |
| MCP Tokens | GET |
/api/v1/private/mcp-tokens/{token_id} |
JWT |
| MCP Tokens | POST |
/api/v1/private/mcp-tokens/{token_id}/revoke |
JWT |
- Version, endpoint, draft and Diff lists accept
page_size=1..200,offset=0..1000000, andsearch(up to 256 UTF-8 bytes). Version search matches the version name. Endpoint search matches method, path, summary, operation ID and tags. Responses includetotalandhas_more. Omittingpage_sizepreserves the legacy list contract. - Paged draft lists contain metadata only and omit
revision,review_revisionanddiff_preview; retrieve/drafts/{id}/content/rawfor a single review/edit snapshot. Paged Diff lists contain identity, version pair, status and timestamps only; retrieve/diffs/{id}for current summary and items. Search matches draft version names or either Diff version name. Draftbranch_idand Difffrom_version_id/to_version_idfilters remain available. Lists do not read schema objects or regenerate historical facts, and sort by creation time then ID descending. - Audit queries accept
limit(default 100, maximum 200),cursor, and RFC3339from(inclusive) /to(exclusive). The response includeshas_moreand, when available,next_cursor. Repeat the same filters with that cursor to continue beyond 200 records. Cursor pages omit a global total; project authorization is checked for every page. - Example:
GET /api/v1/private/projects/{project_id}/documents/{document_id}/versions?page_size=50&offset=50&search=v1. - Client cancellation propagates to database queries, object storage, and interactive AI requests. The API context deadline is 30 seconds for regular requests and 150 seconds for AI/MCP routes, bounded further by any earlier caller deadline. Errors retain HTTP 200 and use envelope statuses
CANCELLED(499) orDEADLINE_EXCEEDED(504). Database rollback/object cleanup has a separate bounded cleanup context. - Background summary jobs survive request cancellation and process restarts. One worker per instance claims jobs with a 180-second lease; each attempt is bounded to 150 seconds. Transient preparation/persistence errors retry up to three attempts with a five-second delay. Graceful shutdown releases durable work for retry; expired leases recover after crashes. Provider response failures are stored as
failedfor explicit retry. Targets, permissions, and generation tokens are rechecked before saving completion. No provider configuration storesskippedimmediately without enqueueing work. - Access logs include
route(the Gin route template),app_code, andapp_statusalongside the transport status, latency and trace ID. Business errors therefore remain distinguishable when transport HTTP status is 200. Request/response bodies and credential values are excluded.
get_latest_schema and get_latest_doc require an explicit branch_id. Missing branches and unsupported arguments return INVALID_ARGUMENT; a version_id cannot be silently ignored. To read an exact historical snapshot, use get_schema_version(project_id, document_id, version_id) or get_doc_version(project_id, document_id, version_id). Both return {version, content} and enforce the same document-type, project membership and read-scope boundaries as current content. Their audit evidence includes the actual version and branch.
get_api_version_draft preserves its metadata fields and adds content (content, hash, draft_id, content_kind, owner_type). Metadata, raw OpenAPI and revision are read from one snapshot. Pass that revision to updates; re-read and reconcile after a stale-revision error. These read-selection tools add no MCP publish capability. The parser/history upgrade described below requires migration 007.
-
Raw content, normalized/stable content and generated Diff snapshots must each fit the storage read limit (
server.max_body_size, default 10 MiB). Oversized objects returnINVALID_ARGUMENTbefore that object is written; previously staged objects are cleaned up and draft/version state is preserved on failure. A source below the request limit can still exceed the storage limit after JSON escaping or normalization. -
x-*extensions in the OpenAPI Paths Object remain literal source data and are not indexed as paths; real path names still require a leading/and at least one operation must be present. -
Local JSON Pointers support object keys, strict array indices, URI-fragment percent decoding and
~0/~1escapes. Local Path Item references are indexed before reading operations, including inherited path parameters and servers. Missing, remote, circular and conflicting Path Item references returnINVALID_ARGUMENTinstead of dropping operations. JSON and YAML numbers preserve exact decimal values through normalization, hashes and PostgreSQL JSONB reloads. -
Parsing checks cancellation and enforces cumulative limits: 16 MiB source, 200,000 visited nodes, 128 traversal levels, a conservative 16 MiB expansion estimate, and 16 MiB serialized operations. Numeric literals are limited to 4,096 characters and parsed exponents to ±1,000,000; canonical nonzero values must also fit PostgreSQL JSONB numeric (at most 131,072 digits before the decimal point and 16,383 after it). Out-of-range values fail with
INVALID_ARGUMENTduring draft creation/update, before publication. NUL characters in strings or object keys are rejected for the same JSONB storage constraint. These limits also count repeated references and YAML aliases. Normal create/update and review parsing use a request snapshot outside the shared Store lock; permissions, draft revisions and publication baseline are checked again before committing. -
OpenAPI 3.1 type arrays are compared as sets. Request narrowing and response widening are breaking; order changes are equivalent. Removing a required response guarantee is breaking. Enum additions are compatible in requests and breaking in responses; enum removals retain the existing conservative breaking classification in both directions. Adding an optional request body or media type does not make its internal required fields mandatory for previous callers.
-
Numeric/length/item/property bounds and
anyOfbranch sets are compared by request/response direction. Numeric bounds compare their effective inclusive/exclusive limits, with OpenAPI 3.0 boolean flags interpreted by that dialect; redundant limits are equivalent, including inside nested schemas.oneOfuses branch additions/removals only for disjoint type branches. Replacing branches, changed unsupported constraints or overlappingoneOfalternatives produceSchema compatibility requires manual review, with warning severity,is_breaking=falseandmust_handle=true. This is not an exhaustive JSON Schema implication solver; warning items must be reviewed rather than treated as proof of compatibility. -
OpenAPI
x-*extension payloads are literal data, including their$refkeys, and response-map extensions are not response statuses. Real named properties and headers beginning withx-still resolve references and participate in comparison. Original extension details remain available. -
Migration
007_parser_facts_and_history_pagesadds derived parser/hash markers, history indexes and a separatemust_handlecolumn. Historical endpoint indexes rebuild lazily from verified immutable raw content, preserve existing endpoint IDs and recover omitted referenced operations. Raw and original normalized objects/hashes remain unchanged. Draft previews and stored Diffs regenerate under parser version 8 when details are read. -
MCP authentication reads the current token by its indexed hash and locks its owner and token in one transaction. Last-use/expiry updates and success/failure audits commit together. Authentication does not load the global application snapshot; revoked tokens and inactive owners remain rejected. MCP tool names, arguments and scopes are unchanged.
-
Parameter
contentmedia schemas participate in comparison alongsideschema; changes between these representations or serialization options require manual review. Required property names are compared even without apropertiesdefinition. Existing media without a schema is treated as unconstrained. Canonicalization ignores genuine schema annotations while preserving identically named properties and all keys inside literal values. -
Private REST Diff items add
old_value_json/new_value_jsonstrings, and endpoint details addjson_previewstrings keyed by section (parameters,request_body,responses,security,servers,normalized_operation,schema_refs). These contain server-formatted JSON for exact rendering, including numeric versus string literals. Original structured fields remain available. Admin prefers the text fields so JavaScript numeric rounding cannot hide a change; clients on older backends fall back to their previous rendering. -
Successful MCP usage changes
last_used_atwithout changing the lifecycleupdated_atused for optimistic concurrency. Lifecycle writes retain the newer usage timestamp and still reject stale status changes. This allows concurrent token use and revocation without restoring stale token state. -
Diff property paths escape literal dots and backslashes in property names, keeping them distinct from nested paths.
-
Schema literal comparisons distinguish empty arrays from
null, including nestedconstvalues and enum/const intersections. Empty/absent equivalence is limited to optional endpoint collections. -
Effective server URLs/variables, response header contracts and request media encodings participate in Diff. Changes require manual review (
must_handle=true, warning severity); server/header descriptions and schema annotations are ignored. Header names are case-insensitive;Content-Typeentries in Header maps are ignored as required by OpenAPI. Missing/empty server lists use the default/server. -
Derived endpoint
normalized_operationincludes the sourceopenapiversion.nullableaffects standard validation in 3.0 only; 3.1 comparisons use type unions and ignore the legacy keyword without changing stored endpoint details or literal values. Local references insidedefaultresponses and encoding entries resolve with the same rules as other response and encoding names.
Audit history supports actor (at most 256 bytes): case-insensitive email substring or exact user ID. Filtering runs before cursor pagination and never expands the caller’s project authorization. Audit DTOs add actor_email and project_name when referenced identities still exist; original IDs remain unchanged. The workbench offers localized action/resource enums and displays explicit system, anonymous, and unavailable-identity labels. Unknown historical enum values retain their raw value in the cell tooltip.