From 766db8c1272f968c0b50ee78a2ba8bdca143a0ce Mon Sep 17 00:00:00 2001 From: munenick Date: Fri, 5 Dec 2025 09:57:38 +0900 Subject: [PATCH 1/3] refactor: plugin --- ...001_add_created_by_plugin_to_documents.sql | 5 + api/openapi/openapi.json | 2 +- .../application/ports/document_repository.rs | 2 + api/src/application/services/documents.rs | 11 +- api/src/application/services/git.rs | 36 ++- api/src/application/services/markdown/mod.rs | 6 +- .../use_cases/documents/create_document.rs | 21 +- .../use_cases/documents/download_document.rs | 1 + .../use_cases/plugins/exec_action.rs | 9 +- api/src/bin/refmd.rs | 176 +++++++++------ api/src/domain/documents/document.rs | 1 + .../repositories/document_repository_sqlx.rs | 9 +- .../db/repositories/public_repository_sqlx.rs | 3 +- api/src/infrastructure/git/workspace.rs | 3 +- api/src/main.rs | 4 +- api/src/presentation/http/auth.rs | 2 +- api/src/presentation/http/documents.rs | 4 + api/src/presentation/http/git.rs | 5 +- api/src/presentation/http/public.rs | 1 + .../file-tree/model/file-tree-context.tsx | 2 + app/src/features/file-tree/model/types.ts | 1 + app/src/features/file-tree/ui/FileNode.tsx | 13 +- .../model/useSecondaryViewerContent.ts | 39 +++- app/src/routeTree.gen.ts | 210 +++++++++--------- app/src/routes/(app)/document/$id.tsx | 5 +- app/src/routes/(app)/settings/index.tsx | 2 +- app/src/shared/api/client/types.gen.ts | 1 + app/src/widgets/document/DocumentPage.tsx | 2 + app/src/widgets/routes/PluginFallback.tsx | 21 ++ 29 files changed, 376 insertions(+), 221 deletions(-) create mode 100644 api/migrations/202604200001_add_created_by_plugin_to_documents.sql diff --git a/api/migrations/202604200001_add_created_by_plugin_to_documents.sql b/api/migrations/202604200001_add_created_by_plugin_to_documents.sql new file mode 100644 index 00000000..d09fe071 --- /dev/null +++ b/api/migrations/202604200001_add_created_by_plugin_to_documents.sql @@ -0,0 +1,5 @@ +ALTER TABLE documents +ADD COLUMN IF NOT EXISTS created_by_plugin TEXT NULL; + +CREATE INDEX IF NOT EXISTS idx_documents_created_by_plugin + ON documents(created_by_plugin); diff --git a/api/openapi/openapi.json b/api/openapi/openapi.json index fe5dcbe0..a96e2379 100644 --- a/api/openapi/openapi.json +++ b/api/openapi/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"api","description":"","license":{"name":""},"version":"0.1.0"},"paths":{"/api/auth/login":{"post":{"tags":["Auth"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}},"security":[{}]}},"/api/auth/logout":{"post":{"tags":["Auth"],"operationId":"logout","responses":{"204":{"description":""}}}},"/api/auth/me":{"get":{"tags":["Auth"],"operationId":"me","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}},"delete":{"tags":["Auth"],"operationId":"delete_account","responses":{"204":{"description":""}}}},"/api/auth/oauth/{provider}":{"post":{"tags":["Auth"],"operationId":"oauth_login","parameters":[{"name":"provider","in":"path","description":"OAuth provider identifier (e.g., google)","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthLoginRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}},"security":[{}]}},"/api/auth/oauth/{provider}/state":{"post":{"tags":["Auth"],"operationId":"oauth_state","parameters":[{"name":"provider","in":"path","description":"OAuth provider identifier","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthStateResponse"}}}}},"security":[{}]}},"/api/auth/providers":{"get":{"tags":["Auth"],"operationId":"list_oauth_providers","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthProvidersResponse"}}}}},"security":[{}]}},"/api/auth/refresh":{"post":{"tags":["Auth"],"operationId":"refresh_session","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshResponse"}}}}}}},"/api/auth/register":{"post":{"tags":["Auth"],"operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{}]}},"/api/auth/sessions":{"get":{"tags":["Auth"],"operationId":"list_sessions","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionResponse"}}}}}}}},"/api/auth/sessions/{id}":{"delete":{"tags":["Auth"],"operationId":"revoke_session","parameters":[{"name":"id","in":"path","description":"Session ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}}},"/api/documents":{"get":{"tags":["Documents"],"operationId":"list_documents","parameters":[{"name":"query","in":"query","description":"Search query","required":false,"schema":{"type":"string","nullable":true}},{"name":"tag","in":"query","description":"Filter by tag","required":false,"schema":{"type":"string","nullable":true}},{"name":"state","in":"query","description":"Filter by document state (active|archived|all)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}}}},"post":{"tags":["Documents"],"operationId":"create_document","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDocumentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/documents/search":{"get":{"tags":["Documents"],"operationId":"search_documents","parameters":[{"name":"q","in":"query","description":"Query","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}}}}}}}},"/api/documents/{id}":{"get":{"tags":["Documents"],"operationId":"get_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}},"delete":{"tags":["Documents"],"operationId":"delete_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Documents"],"operationId":"update_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDocumentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/documents/{id}/archive":{"post":{"tags":["Documents"],"operationId":"archive_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"404":{"description":"Document not found"},"409":{"description":"Document already archived"}}}},"/api/documents/{id}/backlinks":{"get":{"tags":["Documents"],"operationId":"getBacklinks","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacklinksResponse"}}}}}}},"/api/documents/{id}/content":{"get":{"tags":["Documents"],"operationId":"get_document_content","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":""}}},"put":{"tags":["Documents"],"operationId":"update_document_content","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDocumentContentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}},"patch":{"tags":["Documents"],"operationId":"patch_document_content","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchDocumentContentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/documents/{id}/download":{"get":{"tags":["Documents"],"operationId":"download_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}},{"name":"format","in":"query","description":"Download format (see schema for supported values)","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/DownloadFormat"}],"nullable":true}}],"responses":{"200":{"description":"Document download","content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/DocumentDownloadBinary"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Document not found"}}}},"/api/documents/{id}/links":{"get":{"tags":["Documents"],"operationId":"getOutgoingLinks","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutgoingLinksResponse"}}}}}}},"/api/documents/{id}/snapshots":{"get":{"tags":["Documents"],"operationId":"list_document_snapshots","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}},{"name":"limit","in":"query","description":"Maximum number of snapshots to return","required":false,"schema":{"type":"integer","format":"int64","nullable":true}},{"name":"offset","in":"query","description":"Offset for pagination","required":false,"schema":{"type":"integer","format":"int64","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotListResponse"}}}}}}},"/api/documents/{id}/snapshots/{snapshot_id}/diff":{"get":{"tags":["Documents"],"operationId":"get_document_snapshot_diff","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"snapshot_id","in":"path","description":"Snapshot ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}},{"name":"compare","in":"query","description":"Snapshot ID to compare against (defaults to current document state)","required":false,"schema":{"type":"string","format":"uuid","nullable":true}},{"name":"base","in":"query","description":"Base comparison to use when compare is not provided (auto|current|previous)","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/SnapshotDiffBaseParam"}],"nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotDiffResponse"}}}}}}},"/api/documents/{id}/snapshots/{snapshot_id}/download":{"get":{"tags":["Documents"],"operationId":"download_document_snapshot","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"snapshot_id","in":"path","description":"Snapshot ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"Snapshot archive","content":{"application/zip":{"schema":{"$ref":"#/components/schemas/DocumentArchiveBinary"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Snapshot not found"}}}},"/api/documents/{id}/snapshots/{snapshot_id}/restore":{"post":{"tags":["Documents"],"operationId":"restore_document_snapshot","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"snapshot_id","in":"path","description":"Snapshot ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotRestoreResponse"}}}}}}},"/api/documents/{id}/unarchive":{"post":{"tags":["Documents"],"operationId":"unarchive_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"404":{"description":"Document not found"},"409":{"description":"Document is not archived"}}}},"/api/files":{"post":{"tags":["Files"],"summary":"POST /api/files (multipart/form-data)","description":"Fields:\n- file: binary file (required)\n- document_id: uuid (required by current schema)","operationId":"upload_file","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UploadFileMultipart"}}},"required":true},"responses":{"201":{"description":"File uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadFileResponse"}}}}}}},"/api/files/documents/{filename}":{"get":{"tags":["Files"],"summary":"GET /api/files/documents/{filename}?document_id=uuid -> bytes","operationId":"get_file_by_name","parameters":[{"name":"filename","in":"path","description":"File name","required":true,"schema":{"type":"string"}},{"name":"document_id","in":"query","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/files/{id}":{"get":{"tags":["Files"],"summary":"GET /api/files/{id} -> bytes (fallback; primary is /uploads/{filename})","operationId":"get_file","parameters":[{"name":"id","in":"path","description":"File ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/git/changes":{"get":{"tags":["Git"],"operationId":"get_changes","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitChangesResponse"}}}}}}},"/api/git/config":{"get":{"tags":["Git"],"operationId":"get_config","responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/GitConfigResponse"}],"nullable":true}}}}}},"post":{"tags":["Git"],"operationId":"create_or_update_config","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGitConfigRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitConfigResponse"}}}}}},"delete":{"tags":["Git"],"operationId":"delete_config","responses":{"204":{"description":"Deleted"}}}},"/api/git/deinit":{"post":{"tags":["Git"],"operationId":"deinit_repository","responses":{"200":{"description":"OK"}}}},"/api/git/diff/commits/{from}/{to}":{"get":{"tags":["Git"],"operationId":"get_commit_diff","parameters":[{"name":"from","in":"path","description":"From","required":true,"schema":{"type":"string"}},{"name":"to","in":"path","description":"To","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TextDiffResult"}}}}}}}},"/api/git/diff/working":{"get":{"tags":["Git"],"operationId":"get_working_diff","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TextDiffResult"}}}}}}}},"/api/git/gitignore/check":{"post":{"tags":["Git"],"operationId":"check_path_ignored","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckIgnoredRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/api/git/gitignore/patterns":{"get":{"tags":["Git"],"operationId":"get_gitignore_patterns","responses":{"200":{"description":"OK"}}},"post":{"tags":["Git"],"operationId":"add_gitignore_patterns","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPatternsRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/api/git/history":{"get":{"tags":["Git"],"operationId":"get_history","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitHistoryResponse"}}}}}}},"/api/git/ignore/doc/{id}":{"post":{"tags":["Git"],"operationId":"ignore_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/git/ignore/folder/{id}":{"post":{"tags":["Git"],"operationId":"ignore_folder","parameters":[{"name":"id","in":"path","description":"Folder ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/git/init":{"post":{"tags":["Git"],"operationId":"init_repository","responses":{"200":{"description":"OK"}}}},"/api/git/status":{"get":{"tags":["Git"],"operationId":"get_status","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitStatus"}}}}}}},"/api/git/sync":{"post":{"tags":["Git"],"operationId":"sync_now","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitSyncRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitSyncResponse"}}}},"409":{"description":"Conflicts during rebase/pull"}}}},"/api/health":{"get":{"tags":["Health"],"operationId":"health","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResp"}}}}}}},"/api/markdown/render":{"post":{"tags":["Markdown"],"operationId":"render_markdown","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderResponseBody"}}}}}}},"/api/markdown/render-many":{"post":{"tags":["Markdown"],"operationId":"render_markdown_many","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderManyRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderManyResponse"}}}}}}},"/api/me/api-tokens":{"get":{"tags":["Auth"],"operationId":"list_api_tokens","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiTokenItem"}}}}}}},"post":{"tags":["Auth"],"operationId":"create_api_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreateRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreateResponse"}}}}}}},"/api/me/api-tokens/{id}":{"delete":{"tags":["Auth"],"operationId":"revoke_api_token","parameters":[{"name":"id","in":"path","description":"Token ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}}},"/api/me/plugins/install-from-url":{"post":{"tags":["Plugins"],"operationId":"pluginsInstallFromUrl","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallFromUrlBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResponse"}}}}}}},"/api/me/plugins/manifest":{"get":{"tags":["Plugins"],"operationId":"pluginsGetManifest","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ManifestItem"}}}}}}}},"/api/me/plugins/uninstall":{"post":{"tags":["Plugins"],"operationId":"pluginsUninstall","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UninstallBody"}}},"required":true},"responses":{"204":{"description":""}}}},"/api/me/plugins/updates":{"get":{"tags":["Plugins"],"operationId":"sse_updates","responses":{"200":{"description":"Plugin event stream"}}}},"/api/me/shortcuts":{"get":{"tags":["Auth"],"operationId":"get_user_shortcuts","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserShortcutResponse"}}}}}},"put":{"tags":["Auth"],"operationId":"update_user_shortcuts","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserShortcutRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserShortcutResponse"}}}}}}},"/api/plugins/{plugin}/docs/{doc_id}/kv/{key}":{"get":{"tags":["Plugins"],"operationId":"pluginsGetKv","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"key","in":"path","description":"Key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvValueResponse"}}}}}},"put":{"tags":["Plugins"],"operationId":"pluginsPutKv","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"key","in":"path","description":"Key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvValueBody"}}},"required":true},"responses":{"204":{"description":""}}}},"/api/plugins/{plugin}/docs/{doc_id}/records/{kind}":{"get":{"tags":["Plugins"],"operationId":"list_records","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"path","description":"Record kind","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Limit","required":false,"schema":{"type":"integer","format":"int64","nullable":true}},{"name":"offset","in":"query","description":"Offset","required":false,"schema":{"type":"integer","format":"int64","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordsResponse"}}}}}},"post":{"tags":["Plugins"],"operationId":"pluginsCreateRecord","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"path","description":"Record kind","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecordBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{}}}}}}},"/api/plugins/{plugin}/exec/{action}":{"post":{"tags":["Plugins"],"operationId":"pluginsExecAction","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"action","in":"path","description":"Action","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecResultResponse"}}}}}}},"/api/plugins/{plugin}/records/{id}":{"delete":{"tags":["Plugins"],"operationId":"pluginsDeleteRecord","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Record ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Plugins"],"operationId":"pluginsUpdateRecord","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Record ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRecordBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{}}}}}}},"/api/public/documents/{id}":{"get":{"tags":["Public Documents"],"operationId":"get_publish_status","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Published status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"}}}}}},"post":{"tags":["Public Documents"],"operationId":"publish_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"}}}}}},"delete":{"tags":["Public Documents"],"operationId":"unpublish_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Unpublished"}}}},"/api/public/workspaces/{slug}":{"get":{"tags":["Public Documents"],"operationId":"list_workspace_public_documents","parameters":[{"name":"slug","in":"path","description":"Workspace slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Public documents for workspace","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicDocumentSummary"}}}}}}}},"/api/public/workspaces/{slug}/{id}":{"get":{"tags":["Public Documents"],"operationId":"get_public_by_workspace_and_id","parameters":[{"name":"slug","in":"path","description":"Workspace slug","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Document metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/public/workspaces/{slug}/{id}/content":{"get":{"tags":["Public Documents"],"operationId":"get_public_content_by_workspace_and_id","parameters":[{"name":"slug","in":"path","description":"Workspace slug","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Document content"}}}},"/api/shares":{"post":{"tags":["Sharing"],"operationId":"create_share","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShareRequest"}}},"required":true},"responses":{"200":{"description":"Share link created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShareResponse"}}}}}}},"/api/shares/active":{"get":{"tags":["Sharing"],"operationId":"list_active_shares","responses":{"200":{"description":"Active shares","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ActiveShareItem"}}}}}}}},"/api/shares/applicable":{"get":{"tags":["Sharing"],"operationId":"list_applicable_shares","parameters":[{"name":"doc_id","in":"query","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Shares that include the document","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicableShareItem"}}}}}}}},"/api/shares/browse":{"get":{"tags":["Sharing"],"operationId":"browse_share","parameters":[{"name":"token","in":"query","description":"Share token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Share tree","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareBrowseResponse"}}}}}}},"/api/shares/documents/{id}":{"get":{"tags":["Sharing"],"operationId":"list_document_shares","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShareItem"}}}}}}}},"/api/shares/folders/{token}/materialize":{"post":{"tags":["Sharing"],"operationId":"materialize_folder_share","parameters":[{"name":"token","in":"path","description":"Folder share token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Created doc shares","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterializeResponse"}}}}}}},"/api/shares/mounts":{"get":{"tags":["Sharing"],"operationId":"list_share_mounts","responses":{"200":{"description":"Share mounts","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShareMountItem"}}}}}}},"post":{"tags":["Sharing"],"operationId":"create_share_mount","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShareMountRequest"}}},"required":true},"responses":{"200":{"description":"Saved share mount","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareMountItem"}}}}}}},"/api/shares/mounts/{id}":{"delete":{"tags":["Sharing"],"operationId":"delete_share_mount","parameters":[{"name":"id","in":"path","description":"Share mount ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Share mount removed"}}}},"/api/shares/validate":{"get":{"tags":["Sharing"],"operationId":"validate_share_token","parameters":[{"name":"token","in":"query","description":"Share token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareDocumentResponse"}}}}}}},"/api/shares/{token}":{"delete":{"tags":["Sharing"],"operationId":"delete_share","parameters":[{"name":"token","in":"path","description":"Share token","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Share link deleted"}}}},"/api/tags":{"get":{"tags":["Tags"],"operationId":"list_tags","parameters":[{"name":"q","in":"query","description":"Filter contains","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TagItem"}}}}}}}},"/api/workspace-invitations/{token}/accept":{"post":{"tags":["Workspaces"],"operationId":"accept_invitation","parameters":[{"name":"token","in":"path","description":"Invitation token","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":""}}}},"/api/workspaces":{"get":{"tags":["Workspaces"],"operationId":"list_workspaces","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}}},"post":{"tags":["Workspaces"],"operationId":"create_workspace","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}}},"/api/workspaces/{id}":{"get":{"tags":["Workspaces"],"operationId":"get_workspace_detail","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}},"put":{"tags":["Workspaces"],"operationId":"update_workspace","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}},"delete":{"tags":["Workspaces"],"operationId":"delete_workspace","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}}},"/api/workspaces/{id}/download":{"get":{"tags":["Workspaces"],"operationId":"download_workspace_archive","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","description":"Download format (archive only)","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/DownloadFormat"}],"nullable":true}}],"responses":{"200":{"description":"Workspace download","content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/DocumentDownloadBinary"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Workspace not found"}}}},"/api/workspaces/{id}/invitations":{"get":{"tags":["Workspaces"],"operationId":"list_invitations","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceInvitationResponse"}}}}}}},"post":{"tags":["Workspaces"],"operationId":"create_invitation","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceInvitationRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceInvitationResponse"}}}}}}},"/api/workspaces/{id}/invitations/{invitation_id}":{"delete":{"tags":["Workspaces"],"operationId":"revoke_invitation","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"invitation_id","in":"path","description":"Invitation ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceInvitationResponse"}}}}}}},"/api/workspaces/{id}/members":{"get":{"tags":["Workspaces"],"operationId":"list_members","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMemberResponse"}}}}}}}},"/api/workspaces/{id}/members/{user_id}":{"delete":{"tags":["Workspaces"],"operationId":"remove_member","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"user_id","in":"path","description":"Target user ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Workspaces"],"operationId":"update_member_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"user_id","in":"path","description":"Target user ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberRoleRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMemberResponse"}}}}}}},"/api/workspaces/{id}/permissions":{"get":{"tags":["Workspaces"],"operationId":"get_workspace_permissions","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspacePermissionsResponse"}}}}}}},"/api/workspaces/{id}/roles":{"get":{"tags":["Workspaces"],"operationId":"list_roles","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceRoleResponse"}}}}}}},"post":{"tags":["Workspaces"],"operationId":"create_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRoleRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponse"}}}}}}},"/api/workspaces/{id}/roles/{role_id}":{"delete":{"tags":["Workspaces"],"operationId":"delete_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"role_id","in":"path","description":"Role ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Workspaces"],"operationId":"update_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"role_id","in":"path","description":"Role ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRoleRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponse"}}}}}}},"/api/workspaces/{id}/switch":{"post":{"tags":["Workspaces"],"operationId":"switch_workspace","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchWorkspaceResponse"}}}}}}},"/api/yjs/{id}":{"get":{"tags":["Realtime"],"operationId":"axum_ws_entry","parameters":[{"name":"id","in":"path","description":"Document ID (UUID)","required":true,"schema":{"type":"string"}},{"name":"token","in":"query","description":"JWT or share token","required":false,"schema":{"type":"string","nullable":true}},{"name":"Authorization","in":"header","description":"Bearer token (JWT or share token)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"101":{"description":"Switching Protocols (WebSocket upgrade)"},"401":{"description":"Unauthorized"}}}}},"components":{"schemas":{"ActiveShareItem":{"type":"object","required":["id","token","permission","created_at","document_id","document_title","document_type","url"],"properties":{"created_at":{"type":"string","format":"date-time"},"document_id":{"type":"string","format":"uuid"},"document_title":{"type":"string"},"document_type":{"type":"string","description":"'document' or 'folder'"},"expires_at":{"type":"string","format":"date-time","nullable":true},"id":{"type":"string","format":"uuid"},"parent_share_id":{"type":"string","format":"uuid","nullable":true},"permission":{"type":"string"},"token":{"type":"string"},"url":{"type":"string"}}},"AddPatternsRequest":{"type":"object","required":["patterns"],"properties":{"patterns":{"type":"array","items":{"type":"string"}}}},"ApiTokenCreateRequest":{"type":"object","properties":{"name":{"type":"string","example":"Deploy token","nullable":true}}},"ApiTokenCreateResponse":{"type":"object","required":["id","name","created_at","token"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"token":{"type":"string"}}},"ApiTokenItem":{"type":"object","required":["id","name","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string","format":"date-time","nullable":true},"name":{"type":"string"},"revoked_at":{"type":"string","format":"date-time","nullable":true}}},"ApplicableShareItem":{"type":"object","required":["token","permission","scope","excluded"],"properties":{"excluded":{"type":"boolean"},"permission":{"type":"string"},"scope":{"type":"string","description":"'document' or 'folder'"},"token":{"type":"string"}}},"AuthProviderInfoResponse":{"type":"object","required":["id","requires_state","client_ids"],"properties":{"authorization_url":{"type":"string","nullable":true},"client_ids":{"type":"array","items":{"type":"string"}},"id":{"type":"string"},"name":{"type":"string","nullable":true},"redirect_uri":{"type":"string","nullable":true},"requires_state":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}}},"AuthProvidersResponse":{"type":"object","required":["providers"],"properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/AuthProviderInfoResponse"}}}},"BacklinkInfo":{"type":"object","required":["document_id","title","document_type","link_type","link_count"],"properties":{"document_id":{"type":"string"},"document_type":{"type":"string"},"file_path":{"type":"string","nullable":true},"link_count":{"type":"integer","format":"int64"},"link_text":{"type":"string","nullable":true},"link_type":{"type":"string"},"title":{"type":"string"}}},"BacklinksResponse":{"type":"object","required":["backlinks","total_count"],"properties":{"backlinks":{"type":"array","items":{"$ref":"#/components/schemas/BacklinkInfo"}},"total_count":{"type":"integer","minimum":0}}},"CheckIgnoredRequest":{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}},"CreateDocumentRequest":{"type":"object","properties":{"parent_id":{"type":"string","format":"uuid","nullable":true},"title":{"type":"string","nullable":true},"type":{"type":"string","nullable":true}}},"CreateGitConfigRequest":{"type":"object","required":["repository_url","auth_type","auth_data"],"properties":{"auth_data":{},"auth_type":{"type":"string"},"auto_sync":{"type":"boolean","nullable":true},"branch_name":{"type":"string","nullable":true},"repository_url":{"type":"string"}}},"CreateRecordBody":{"type":"object","required":["data"],"properties":{"data":{}}},"CreateShareMountRequest":{"type":"object","required":["token"],"properties":{"parent_folder_id":{"type":"string","format":"uuid","nullable":true},"token":{"type":"string"}}},"CreateShareRequest":{"type":"object","required":["document_id"],"properties":{"document_id":{"type":"string","format":"uuid"},"expires_at":{"type":"string","format":"date-time","nullable":true},"permission":{"type":"string","nullable":true}}},"CreateShareResponse":{"type":"object","required":["token","url"],"properties":{"token":{"type":"string"},"url":{"type":"string"}}},"CreateWorkspaceInvitationRequest":{"type":"object","required":["email","role_kind"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"email":{"type":"string"},"expires_at":{"type":"string","format":"date-time","nullable":true},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"CreateWorkspaceRequest":{"type":"object","required":["name"],"properties":{"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"name":{"type":"string"}}},"CreateWorkspaceRoleRequest":{"type":"object","required":["name","base_role"],"properties":{"base_role":{"type":"string"},"description":{"type":"string","nullable":true},"name":{"type":"string"},"overrides":{"type":"array","items":{"$ref":"#/components/schemas/PermissionOverridePayload"},"nullable":true},"priority":{"type":"integer","format":"int32","nullable":true}}},"Document":{"type":"object","required":["id","owner_id","workspace_id","title","type","created_at","updated_at","slug","desired_path"],"properties":{"archived_at":{"type":"string","format":"date-time","nullable":true},"archived_by":{"type":"string","format":"uuid","nullable":true},"archived_parent_id":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid","nullable":true},"desired_path":{"type":"string"},"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true},"path":{"type":"string","nullable":true},"slug":{"type":"string"},"title":{"type":"string"},"type":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"workspace_id":{"type":"string","format":"uuid"}}},"DocumentArchiveBinary":{"type":"string","format":"binary"},"DocumentDownloadBinary":{"type":"string","format":"binary"},"DocumentListResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Document"}}}},"DocumentPatchOperationRequest":{"oneOf":[{"type":"object","required":["offset","text","op"],"properties":{"offset":{"type":"integer","minimum":0},"op":{"type":"string","enum":["insert"]},"text":{"type":"string"}}},{"type":"object","required":["offset","length","op"],"properties":{"length":{"type":"integer","minimum":0},"offset":{"type":"integer","minimum":0},"op":{"type":"string","enum":["delete"]}}},{"type":"object","required":["offset","length","text","op"],"properties":{"length":{"type":"integer","minimum":0},"offset":{"type":"integer","minimum":0},"op":{"type":"string","enum":["replace"]},"text":{"type":"string"}}}],"discriminator":{"propertyName":"op"}},"DownloadDocumentQuery":{"type":"object","properties":{"format":{"$ref":"#/components/schemas/DownloadFormat"},"token":{"type":"string","nullable":true}}},"DownloadFormat":{"type":"string","enum":["archive","markdown","html","html5","pdf","docx","latex","beamer","context","man","mediawiki","dokuwiki","textile","org","texinfo","opml","docbook","opendocument","odt","rtf","epub","epub3","fb2","asciidoc","icml","slidy","slideous","dzslides","revealjs","s5","json","plain","commonmark","commonmark_x","markdown_strict","markdown_phpextra","markdown_github","rst","native","haddock"]},"DownloadWorkspaceQuery":{"type":"object","properties":{"format":{"$ref":"#/components/schemas/DownloadFormat"}}},"ExecBody":{"type":"object","properties":{"payload":{"nullable":true}}},"ExecResultResponse":{"type":"object","required":["ok","effects"],"properties":{"data":{"nullable":true},"effects":{"type":"array","items":{}},"error":{"nullable":true},"ok":{"type":"boolean"}}},"GitChangeItem":{"type":"object","required":["path","status"],"properties":{"path":{"type":"string"},"status":{"type":"string"}}},"GitChangesResponse":{"type":"object","required":["files"],"properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/GitChangeItem"}}}},"GitCommitItem":{"type":"object","required":["hash","message","author_name","author_email","time"],"properties":{"author_email":{"type":"string"},"author_name":{"type":"string"},"hash":{"type":"string"},"message":{"type":"string"},"time":{"type":"string","format":"date-time"}}},"GitConfigResponse":{"type":"object","required":["id","repository_url","branch_name","auth_type","auto_sync","created_at","updated_at"],"properties":{"auth_type":{"type":"string"},"auto_sync":{"type":"boolean"},"branch_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"remote_check":{"allOf":[{"$ref":"#/components/schemas/GitRemoteCheckResponse"}],"nullable":true},"repository_url":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"GitHistoryResponse":{"type":"object","required":["commits"],"properties":{"commits":{"type":"array","items":{"$ref":"#/components/schemas/GitCommitItem"}}}},"GitRemoteCheckResponse":{"type":"object","required":["ok","message"],"properties":{"message":{"type":"string"},"ok":{"type":"boolean"},"reason":{"type":"string","nullable":true}}},"GitStatus":{"type":"object","required":["repository_initialized","has_remote","uncommitted_changes","untracked_files","sync_enabled"],"properties":{"current_branch":{"type":"string","nullable":true},"has_remote":{"type":"boolean"},"last_sync":{"type":"string","format":"date-time","nullable":true},"last_sync_commit_hash":{"type":"string","nullable":true},"last_sync_message":{"type":"string","nullable":true},"last_sync_status":{"type":"string","nullable":true},"repository_initialized":{"type":"boolean"},"sync_enabled":{"type":"boolean"},"uncommitted_changes":{"type":"integer","format":"int32","minimum":0},"untracked_files":{"type":"integer","format":"int32","minimum":0}}},"GitSyncRequest":{"type":"object","properties":{"force":{"type":"boolean","nullable":true},"full_scan":{"type":"boolean","nullable":true},"message":{"type":"string","nullable":true},"skip_push":{"type":"boolean","nullable":true}}},"GitSyncResponse":{"type":"object","required":["success","message","files_changed"],"properties":{"commit_hash":{"type":"string","nullable":true},"files_changed":{"type":"integer","format":"int32","minimum":0},"message":{"type":"string"},"success":{"type":"boolean"}}},"HealthResp":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}}},"InstallFromUrlBody":{"type":"object","required":["url"],"properties":{"token":{"type":"string","nullable":true},"url":{"type":"string"}}},"InstallResponse":{"type":"object","required":["id","version"],"properties":{"id":{"type":"string"},"version":{"type":"string"}}},"KvValueBody":{"type":"object","required":["value"],"properties":{"value":{}}},"KvValueResponse":{"type":"object","required":["value"],"properties":{"value":{}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"remember_me":{"type":"boolean"}}},"LoginResponse":{"type":"object","required":["access_token","user"],"properties":{"access_token":{"type":"string"},"user":{"$ref":"#/components/schemas/UserResponse"}}},"ManifestItem":{"type":"object","required":["id","version","scope","mounts","frontend","permissions","config","ui"],"properties":{"author":{"type":"string","nullable":true},"config":{},"frontend":{},"id":{"type":"string"},"mounts":{"type":"array","items":{"type":"string"}},"name":{"type":"string","nullable":true},"permissions":{"type":"array","items":{"type":"string"}},"repository":{"type":"string","nullable":true},"scope":{"type":"string"},"ui":{},"version":{"type":"string"}}},"MaterializeResponse":{"type":"object","required":["created"],"properties":{"created":{"type":"integer","format":"int64"}}},"OAuthLoginRequest":{"type":"object","properties":{"code":{"type":"string","nullable":true},"credential":{"type":"string","nullable":true},"redirect_uri":{"type":"string","nullable":true},"remember_me":{"type":"boolean"},"state":{"type":"string","nullable":true}}},"OAuthStateResponse":{"type":"object","required":["state"],"properties":{"state":{"type":"string"}}},"OutgoingLink":{"type":"object","required":["document_id","title","document_type","link_type"],"properties":{"document_id":{"type":"string"},"document_type":{"type":"string"},"file_path":{"type":"string","nullable":true},"link_text":{"type":"string","nullable":true},"link_type":{"type":"string"},"position_end":{"type":"integer","format":"int32","nullable":true},"position_start":{"type":"integer","format":"int32","nullable":true},"title":{"type":"string"}}},"OutgoingLinksResponse":{"type":"object","required":["links","total_count"],"properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/OutgoingLink"}},"total_count":{"type":"integer","minimum":0}}},"PatchDocumentContentRequest":{"type":"object","required":["operations"],"properties":{"operations":{"type":"array","items":{"$ref":"#/components/schemas/DocumentPatchOperationRequest"}}}},"PermissionOverridePayload":{"type":"object","required":["permission","allowed"],"properties":{"allowed":{"type":"boolean"},"permission":{"type":"string"}}},"PlaceholderItemPayload":{"type":"object","required":["kind","id","code"],"properties":{"code":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"}}},"PublicDocumentSummary":{"type":"object","required":["id","title","updated_at","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"published_at":{"type":"string","format":"date-time"},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"PublishResponse":{"type":"object","required":["slug","public_url"],"properties":{"public_url":{"type":"string"},"slug":{"type":"string"}}},"RecordsResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{}}}},"RefreshResponse":{"type":"object","required":["access_token"],"properties":{"access_token":{"type":"string"}}},"RegisterRequest":{"type":"object","required":["email","name","password"],"properties":{"email":{"type":"string"},"name":{"type":"string"},"password":{"type":"string"}}},"RenderManyRequest":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RenderRequest"}}}},"RenderManyResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RenderResponseBody"}}}},"RenderOptionsPayload":{"type":"object","properties":{"absolute_attachments":{"type":"boolean","default":null,"nullable":true},"base_origin":{"type":"string","default":null,"nullable":true},"doc_id":{"type":"string","format":"uuid","default":null,"nullable":true},"features":{"type":"array","items":{"type":"string"},"default":null,"nullable":true},"flavor":{"type":"string","default":null,"nullable":true},"hardbreaks":{"type":"boolean","default":null,"nullable":true},"sanitize":{"type":"boolean","default":null,"nullable":true},"theme":{"type":"string","default":null,"nullable":true},"token":{"type":"string","default":null,"nullable":true}}},"RenderRequest":{"type":"object","required":["text"],"properties":{"options":{"$ref":"#/components/schemas/RenderOptionsPayload"},"text":{"type":"string"}}},"RenderResponseBody":{"type":"object","required":["html","hash"],"properties":{"hash":{"type":"string"},"html":{"type":"string"},"placeholders":{"type":"array","items":{"$ref":"#/components/schemas/PlaceholderItemPayload"}}}},"SearchResult":{"type":"object","required":["id","title","document_type","updated_at"],"properties":{"document_type":{"type":"string"},"id":{"type":"string","format":"uuid"},"path":{"type":"string","nullable":true},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"SessionResponse":{"type":"object","required":["id","workspace_id","remember_me","created_at","last_seen_at","expires_at","current"],"properties":{"created_at":{"type":"string","format":"date-time"},"current":{"type":"boolean"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"ip_address":{"type":"string","nullable":true},"last_seen_at":{"type":"string","format":"date-time"},"remember_me":{"type":"boolean"},"user_agent":{"type":"string","nullable":true},"workspace_id":{"type":"string","format":"uuid"}}},"ShareBrowseResponse":{"type":"object","required":["tree"],"properties":{"tree":{"type":"array","items":{"$ref":"#/components/schemas/ShareBrowseTreeItem"}}}},"ShareBrowseTreeItem":{"type":"object","required":["id","title","type","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true},"title":{"type":"string"},"type":{"type":"string","example":"document"},"updated_at":{"type":"string","format":"date-time"}}},"ShareDocumentResponse":{"type":"object","required":["id","title","permission"],"properties":{"content":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"permission":{"type":"string"},"title":{"type":"string"}}},"ShareItem":{"type":"object","required":["id","token","permission","url","scope"],"properties":{"expires_at":{"type":"string","format":"date-time","nullable":true},"id":{"type":"string","format":"uuid"},"parent_share_id":{"type":"string","format":"uuid","description":"If present, this document share was materialized from a folder share","nullable":true},"permission":{"type":"string"},"scope":{"type":"string","description":"document | folder"},"token":{"type":"string"},"url":{"type":"string"}}},"ShareMountItem":{"type":"object","required":["id","token","target_document_id","target_document_type","target_title","permission","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"parent_folder_id":{"type":"string","format":"uuid","nullable":true},"permission":{"type":"string"},"target_document_id":{"type":"string","format":"uuid"},"target_document_type":{"type":"string"},"target_title":{"type":"string"},"token":{"type":"string"}}},"SnapshotDiffBaseParam":{"type":"string","enum":["auto","current","previous"]},"SnapshotDiffKind":{"type":"string","enum":["current","snapshot"]},"SnapshotDiffResponse":{"type":"object","required":["base","target","diff"],"properties":{"base":{"$ref":"#/components/schemas/SnapshotDiffSideResponse"},"diff":{"$ref":"#/components/schemas/TextDiffResult"},"target":{"$ref":"#/components/schemas/SnapshotDiffSideResponse"}}},"SnapshotDiffSideResponse":{"type":"object","required":["kind","markdown"],"properties":{"kind":{"$ref":"#/components/schemas/SnapshotDiffKind"},"markdown":{"type":"string"},"snapshot":{"allOf":[{"$ref":"#/components/schemas/SnapshotSummary"}],"nullable":true}}},"SnapshotListResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotSummary"}}}},"SnapshotRestoreResponse":{"type":"object","required":["snapshot"],"properties":{"snapshot":{"$ref":"#/components/schemas/SnapshotSummary"}}},"SnapshotSummary":{"type":"object","required":["id","document_id","label","kind","created_at","byte_size","content_hash"],"properties":{"byte_size":{"type":"integer","format":"int64"},"content_hash":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid","nullable":true},"document_id":{"type":"string","format":"uuid"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"label":{"type":"string"},"notes":{"type":"string","nullable":true}}},"SwitchWorkspaceResponse":{"type":"object","required":["access_token"],"properties":{"access_token":{"type":"string"}}},"TagItem":{"type":"object","required":["name","count"],"properties":{"count":{"type":"integer","format":"int64"},"name":{"type":"string"}}},"TextDiffLine":{"type":"object","required":["line_type","content"],"properties":{"content":{"type":"string"},"line_type":{"$ref":"#/components/schemas/TextDiffLineType"},"new_line_number":{"type":"integer","format":"int32","nullable":true,"minimum":0},"old_line_number":{"type":"integer","format":"int32","nullable":true,"minimum":0}}},"TextDiffLineType":{"type":"string","enum":["added","deleted","context"]},"TextDiffResult":{"type":"object","required":["file_path","diff_lines"],"properties":{"diff_lines":{"type":"array","items":{"$ref":"#/components/schemas/TextDiffLine"}},"file_path":{"type":"string"},"new_content":{"type":"string","nullable":true},"old_content":{"type":"string","nullable":true}}},"UninstallBody":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}},"UpdateDocumentContentRequest":{"type":"object","required":["content"],"properties":{"content":{"type":"string"}}},"UpdateDocumentRequest":{"type":"object","properties":{"parent_id":{"type":"string","nullable":true},"title":{"type":"string","nullable":true}}},"UpdateGitConfigRequest":{"type":"object","properties":{"auth_data":{"nullable":true},"auth_type":{"type":"string","nullable":true},"auto_sync":{"type":"boolean","nullable":true},"branch_name":{"type":"string","nullable":true},"repository_url":{"type":"string","nullable":true}}},"UpdateMemberRoleRequest":{"type":"object","required":["role_kind"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"UpdateRecordBody":{"type":"object","required":["patch"],"properties":{"patch":{}}},"UpdateUserShortcutRequest":{"type":"object","properties":{"bindings":{"type":"object"},"leader_key":{"type":"string","example":"","nullable":true}}},"UpdateWorkspaceRequest":{"type":"object","properties":{"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}}},"UpdateWorkspaceRoleRequest":{"type":"object","properties":{"base_role":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"overrides":{"type":"array","items":{"$ref":"#/components/schemas/PermissionOverridePayload"},"nullable":true},"priority":{"type":"integer","format":"int32","nullable":true}}},"UploadFileMultipart":{"type":"object","required":["file","document_id"],"properties":{"document_id":{"type":"string","format":"uuid","description":"Target document ID"},"file":{"type":"string","format":"binary","description":"File to upload"}}},"UploadFileResponse":{"type":"object","required":["id","url","filename","size"],"properties":{"content_type":{"type":"string","nullable":true},"filename":{"type":"string"},"id":{"type":"string","format":"uuid"},"size":{"type":"integer","format":"int64"},"url":{"type":"string"}}},"UserResponse":{"type":"object","required":["id","email","name","workspaces"],"properties":{"active_workspace":{"allOf":[{"$ref":"#/components/schemas/WorkspaceMembershipResponse"}],"nullable":true},"active_workspace_id":{"type":"string","format":"uuid","nullable":true},"active_workspace_permissions":{"type":"array","items":{"type":"string"}},"email":{"type":"string"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMembershipResponse"}}}},"UserShortcutResponse":{"type":"object","required":["bindings"],"properties":{"bindings":{"type":"object"},"leader_key":{"type":"string","example":"","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"WorkspaceInvitationResponse":{"type":"object","required":["id","workspace_id","email","role_kind","invited_by","token","created_at"],"properties":{"accepted_at":{"type":"string","format":"date-time","nullable":true},"accepted_by":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"},"custom_role_id":{"type":"string","format":"uuid","nullable":true},"email":{"type":"string"},"expires_at":{"type":"string","format":"date-time","nullable":true},"id":{"type":"string","format":"uuid"},"invited_by":{"type":"string","format":"uuid"},"revoked_at":{"type":"string","format":"date-time","nullable":true},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true},"token":{"type":"string"},"workspace_id":{"type":"string","format":"uuid"}}},"WorkspaceMemberResponse":{"type":"object","required":["workspace_id","user_id","email","name","role_kind","is_default"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"email":{"type":"string"},"is_default":{"type":"boolean"},"name":{"type":"string"},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true},"user_id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"}}},"WorkspaceMembershipResponse":{"type":"object","required":["id","name","slug","is_personal","role_kind","is_default"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"is_default":{"type":"boolean"},"is_personal":{"type":"boolean"},"name":{"type":"string"},"role_kind":{"type":"string"},"slug":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"WorkspacePermissionsResponse":{"type":"object","required":["workspace_id","permissions"],"properties":{"permissions":{"type":"array","items":{"type":"string"}},"workspace_id":{"type":"string","format":"uuid"}}},"WorkspaceResponse":{"type":"object","required":["id","name","slug","is_personal","role_kind","is_default"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"is_default":{"type":"boolean"},"is_personal":{"type":"boolean"},"name":{"type":"string"},"role_kind":{"type":"string"},"slug":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"WorkspaceRoleResponse":{"type":"object","required":["id","workspace_id","name","base_role","priority","overrides"],"properties":{"base_role":{"type":"string"},"description":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"overrides":{"type":"array","items":{"$ref":"#/components/schemas/PermissionOverridePayload"}},"priority":{"type":"integer","format":"int32"},"workspace_id":{"type":"string","format":"uuid"}}}}},"tags":[{"name":"Auth","description":"Authentication"},{"name":"Documents","description":"Documents management"},{"name":"Files","description":"File management"},{"name":"Sharing","description":"Document sharing"},{"name":"Public Documents","description":"Public pages"},{"name":"Realtime","description":"Yjs WebSocket endpoint (/yjs/:id)"},{"name":"Git","description":"Git integration"},{"name":"Markdown","description":"Markdown rendering"},{"name":"Plugins","description":"Plugins management & data APIs"},{"name":"Health","description":"System health checks"}]} +{"openapi":"3.0.3","info":{"title":"api","description":"","license":{"name":""},"version":"0.1.0"},"paths":{"/api/auth/login":{"post":{"tags":["Auth"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}},"security":[{}]}},"/api/auth/logout":{"post":{"tags":["Auth"],"operationId":"logout","responses":{"204":{"description":""}}}},"/api/auth/me":{"get":{"tags":["Auth"],"operationId":"me","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}},"delete":{"tags":["Auth"],"operationId":"delete_account","responses":{"204":{"description":""}}}},"/api/auth/oauth/{provider}":{"post":{"tags":["Auth"],"operationId":"oauth_login","parameters":[{"name":"provider","in":"path","description":"OAuth provider identifier (e.g., google)","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthLoginRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}},"security":[{}]}},"/api/auth/oauth/{provider}/state":{"post":{"tags":["Auth"],"operationId":"oauth_state","parameters":[{"name":"provider","in":"path","description":"OAuth provider identifier","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthStateResponse"}}}}},"security":[{}]}},"/api/auth/providers":{"get":{"tags":["Auth"],"operationId":"list_oauth_providers","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthProvidersResponse"}}}}},"security":[{}]}},"/api/auth/refresh":{"post":{"tags":["Auth"],"operationId":"refresh_session","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshResponse"}}}}}}},"/api/auth/register":{"post":{"tags":["Auth"],"operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{}]}},"/api/auth/sessions":{"get":{"tags":["Auth"],"operationId":"list_sessions","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionResponse"}}}}}}}},"/api/auth/sessions/{id}":{"delete":{"tags":["Auth"],"operationId":"revoke_session","parameters":[{"name":"id","in":"path","description":"Session ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}}},"/api/documents":{"get":{"tags":["Documents"],"operationId":"list_documents","parameters":[{"name":"query","in":"query","description":"Search query","required":false,"schema":{"type":"string","nullable":true}},{"name":"tag","in":"query","description":"Filter by tag","required":false,"schema":{"type":"string","nullable":true}},{"name":"state","in":"query","description":"Filter by document state (active|archived|all)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}}}},"post":{"tags":["Documents"],"operationId":"create_document","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDocumentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/documents/search":{"get":{"tags":["Documents"],"operationId":"search_documents","parameters":[{"name":"q","in":"query","description":"Query","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}}}}}}}},"/api/documents/{id}":{"get":{"tags":["Documents"],"operationId":"get_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}},"delete":{"tags":["Documents"],"operationId":"delete_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Documents"],"operationId":"update_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDocumentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/documents/{id}/archive":{"post":{"tags":["Documents"],"operationId":"archive_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"404":{"description":"Document not found"},"409":{"description":"Document already archived"}}}},"/api/documents/{id}/backlinks":{"get":{"tags":["Documents"],"operationId":"getBacklinks","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacklinksResponse"}}}}}}},"/api/documents/{id}/content":{"get":{"tags":["Documents"],"operationId":"get_document_content","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":""}}},"put":{"tags":["Documents"],"operationId":"update_document_content","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDocumentContentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}},"patch":{"tags":["Documents"],"operationId":"patch_document_content","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchDocumentContentRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/documents/{id}/download":{"get":{"tags":["Documents"],"operationId":"download_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}},{"name":"format","in":"query","description":"Download format (see schema for supported values)","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/DownloadFormat"}],"nullable":true}}],"responses":{"200":{"description":"Document download","content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/DocumentDownloadBinary"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Document not found"}}}},"/api/documents/{id}/links":{"get":{"tags":["Documents"],"operationId":"getOutgoingLinks","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutgoingLinksResponse"}}}}}}},"/api/documents/{id}/snapshots":{"get":{"tags":["Documents"],"operationId":"list_document_snapshots","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}},{"name":"limit","in":"query","description":"Maximum number of snapshots to return","required":false,"schema":{"type":"integer","format":"int64","nullable":true}},{"name":"offset","in":"query","description":"Offset for pagination","required":false,"schema":{"type":"integer","format":"int64","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotListResponse"}}}}}}},"/api/documents/{id}/snapshots/{snapshot_id}/diff":{"get":{"tags":["Documents"],"operationId":"get_document_snapshot_diff","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"snapshot_id","in":"path","description":"Snapshot ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}},{"name":"compare","in":"query","description":"Snapshot ID to compare against (defaults to current document state)","required":false,"schema":{"type":"string","format":"uuid","nullable":true}},{"name":"base","in":"query","description":"Base comparison to use when compare is not provided (auto|current|previous)","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/SnapshotDiffBaseParam"}],"nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotDiffResponse"}}}}}}},"/api/documents/{id}/snapshots/{snapshot_id}/download":{"get":{"tags":["Documents"],"operationId":"download_document_snapshot","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"snapshot_id","in":"path","description":"Snapshot ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"Snapshot archive","content":{"application/zip":{"schema":{"$ref":"#/components/schemas/DocumentArchiveBinary"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Snapshot not found"}}}},"/api/documents/{id}/snapshots/{snapshot_id}/restore":{"post":{"tags":["Documents"],"operationId":"restore_document_snapshot","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"snapshot_id","in":"path","description":"Snapshot ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","description":"Share token (optional)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotRestoreResponse"}}}}}}},"/api/documents/{id}/unarchive":{"post":{"tags":["Documents"],"operationId":"unarchive_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}},"404":{"description":"Document not found"},"409":{"description":"Document is not archived"}}}},"/api/files":{"post":{"tags":["Files"],"summary":"POST /api/files (multipart/form-data)","description":"Fields:\n- file: binary file (required)\n- document_id: uuid (required by current schema)","operationId":"upload_file","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UploadFileMultipart"}}},"required":true},"responses":{"201":{"description":"File uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadFileResponse"}}}}}}},"/api/files/documents/{filename}":{"get":{"tags":["Files"],"summary":"GET /api/files/documents/{filename}?document_id=uuid -> bytes","operationId":"get_file_by_name","parameters":[{"name":"filename","in":"path","description":"File name","required":true,"schema":{"type":"string"}},{"name":"document_id","in":"query","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/files/{id}":{"get":{"tags":["Files"],"summary":"GET /api/files/{id} -> bytes (fallback; primary is /uploads/{filename})","operationId":"get_file","parameters":[{"name":"id","in":"path","description":"File ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/git/changes":{"get":{"tags":["Git"],"operationId":"get_changes","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitChangesResponse"}}}}}}},"/api/git/config":{"get":{"tags":["Git"],"operationId":"get_config","responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/GitConfigResponse"}],"nullable":true}}}}}},"post":{"tags":["Git"],"operationId":"create_or_update_config","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGitConfigRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitConfigResponse"}}}}}},"delete":{"tags":["Git"],"operationId":"delete_config","responses":{"204":{"description":"Deleted"}}}},"/api/git/deinit":{"post":{"tags":["Git"],"operationId":"deinit_repository","responses":{"200":{"description":"OK"}}}},"/api/git/diff/commits/{from}/{to}":{"get":{"tags":["Git"],"operationId":"get_commit_diff","parameters":[{"name":"from","in":"path","description":"From","required":true,"schema":{"type":"string"}},{"name":"to","in":"path","description":"To","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TextDiffResult"}}}}}}}},"/api/git/diff/working":{"get":{"tags":["Git"],"operationId":"get_working_diff","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TextDiffResult"}}}}}}}},"/api/git/gitignore/check":{"post":{"tags":["Git"],"operationId":"check_path_ignored","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckIgnoredRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/api/git/gitignore/patterns":{"get":{"tags":["Git"],"operationId":"get_gitignore_patterns","responses":{"200":{"description":"OK"}}},"post":{"tags":["Git"],"operationId":"add_gitignore_patterns","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPatternsRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/api/git/history":{"get":{"tags":["Git"],"operationId":"get_history","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitHistoryResponse"}}}}}}},"/api/git/ignore/doc/{id}":{"post":{"tags":["Git"],"operationId":"ignore_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/git/ignore/folder/{id}":{"post":{"tags":["Git"],"operationId":"ignore_folder","parameters":[{"name":"id","in":"path","description":"Folder ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/git/init":{"post":{"tags":["Git"],"operationId":"init_repository","responses":{"200":{"description":"OK"}}}},"/api/git/status":{"get":{"tags":["Git"],"operationId":"get_status","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitStatus"}}}}}}},"/api/git/sync":{"post":{"tags":["Git"],"operationId":"sync_now","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitSyncRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitSyncResponse"}}}},"409":{"description":"Conflicts during rebase/pull"}}}},"/api/health":{"get":{"tags":["Health"],"operationId":"health","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResp"}}}}}}},"/api/markdown/render":{"post":{"tags":["Markdown"],"operationId":"render_markdown","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderResponseBody"}}}}}}},"/api/markdown/render-many":{"post":{"tags":["Markdown"],"operationId":"render_markdown_many","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderManyRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderManyResponse"}}}}}}},"/api/me/api-tokens":{"get":{"tags":["Auth"],"operationId":"list_api_tokens","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiTokenItem"}}}}}}},"post":{"tags":["Auth"],"operationId":"create_api_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreateRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreateResponse"}}}}}}},"/api/me/api-tokens/{id}":{"delete":{"tags":["Auth"],"operationId":"revoke_api_token","parameters":[{"name":"id","in":"path","description":"Token ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}}},"/api/me/plugins/install-from-url":{"post":{"tags":["Plugins"],"operationId":"pluginsInstallFromUrl","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallFromUrlBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallResponse"}}}}}}},"/api/me/plugins/manifest":{"get":{"tags":["Plugins"],"operationId":"pluginsGetManifest","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ManifestItem"}}}}}}}},"/api/me/plugins/uninstall":{"post":{"tags":["Plugins"],"operationId":"pluginsUninstall","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UninstallBody"}}},"required":true},"responses":{"204":{"description":""}}}},"/api/me/plugins/updates":{"get":{"tags":["Plugins"],"operationId":"sse_updates","responses":{"200":{"description":"Plugin event stream"}}}},"/api/me/shortcuts":{"get":{"tags":["Auth"],"operationId":"get_user_shortcuts","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserShortcutResponse"}}}}}},"put":{"tags":["Auth"],"operationId":"update_user_shortcuts","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserShortcutRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserShortcutResponse"}}}}}}},"/api/plugins/{plugin}/docs/{doc_id}/kv/{key}":{"get":{"tags":["Plugins"],"operationId":"pluginsGetKv","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"key","in":"path","description":"Key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvValueResponse"}}}}}},"put":{"tags":["Plugins"],"operationId":"pluginsPutKv","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"key","in":"path","description":"Key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvValueBody"}}},"required":true},"responses":{"204":{"description":""}}}},"/api/plugins/{plugin}/docs/{doc_id}/records/{kind}":{"get":{"tags":["Plugins"],"operationId":"list_records","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"path","description":"Record kind","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Limit","required":false,"schema":{"type":"integer","format":"int64","nullable":true}},{"name":"offset","in":"query","description":"Offset","required":false,"schema":{"type":"integer","format":"int64","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordsResponse"}}}}}},"post":{"tags":["Plugins"],"operationId":"pluginsCreateRecord","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"doc_id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"path","description":"Record kind","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecordBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{}}}}}}},"/api/plugins/{plugin}/exec/{action}":{"post":{"tags":["Plugins"],"operationId":"pluginsExecAction","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"action","in":"path","description":"Action","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecResultResponse"}}}}}}},"/api/plugins/{plugin}/records/{id}":{"delete":{"tags":["Plugins"],"operationId":"pluginsDeleteRecord","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Record ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Plugins"],"operationId":"pluginsUpdateRecord","parameters":[{"name":"plugin","in":"path","description":"Plugin ID","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Record ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRecordBody"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{}}}}}}},"/api/public/documents/{id}":{"get":{"tags":["Public Documents"],"operationId":"get_publish_status","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Published status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"}}}}}},"post":{"tags":["Public Documents"],"operationId":"publish_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"}}}}}},"delete":{"tags":["Public Documents"],"operationId":"unpublish_document","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Unpublished"}}}},"/api/public/workspaces/{slug}":{"get":{"tags":["Public Documents"],"operationId":"list_workspace_public_documents","parameters":[{"name":"slug","in":"path","description":"Workspace slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Public documents for workspace","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicDocumentSummary"}}}}}}}},"/api/public/workspaces/{slug}/{id}":{"get":{"tags":["Public Documents"],"operationId":"get_public_by_workspace_and_id","parameters":[{"name":"slug","in":"path","description":"Workspace slug","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Document metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}}}}}},"/api/public/workspaces/{slug}/{id}/content":{"get":{"tags":["Public Documents"],"operationId":"get_public_content_by_workspace_and_id","parameters":[{"name":"slug","in":"path","description":"Workspace slug","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Document content"}}}},"/api/shares":{"post":{"tags":["Sharing"],"operationId":"create_share","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShareRequest"}}},"required":true},"responses":{"200":{"description":"Share link created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShareResponse"}}}}}}},"/api/shares/active":{"get":{"tags":["Sharing"],"operationId":"list_active_shares","responses":{"200":{"description":"Active shares","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ActiveShareItem"}}}}}}}},"/api/shares/applicable":{"get":{"tags":["Sharing"],"operationId":"list_applicable_shares","parameters":[{"name":"doc_id","in":"query","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Shares that include the document","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicableShareItem"}}}}}}}},"/api/shares/browse":{"get":{"tags":["Sharing"],"operationId":"browse_share","parameters":[{"name":"token","in":"query","description":"Share token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Share tree","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareBrowseResponse"}}}}}}},"/api/shares/documents/{id}":{"get":{"tags":["Sharing"],"operationId":"list_document_shares","parameters":[{"name":"id","in":"path","description":"Document ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShareItem"}}}}}}}},"/api/shares/folders/{token}/materialize":{"post":{"tags":["Sharing"],"operationId":"materialize_folder_share","parameters":[{"name":"token","in":"path","description":"Folder share token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Created doc shares","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterializeResponse"}}}}}}},"/api/shares/mounts":{"get":{"tags":["Sharing"],"operationId":"list_share_mounts","responses":{"200":{"description":"Share mounts","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShareMountItem"}}}}}}},"post":{"tags":["Sharing"],"operationId":"create_share_mount","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShareMountRequest"}}},"required":true},"responses":{"200":{"description":"Saved share mount","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareMountItem"}}}}}}},"/api/shares/mounts/{id}":{"delete":{"tags":["Sharing"],"operationId":"delete_share_mount","parameters":[{"name":"id","in":"path","description":"Share mount ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Share mount removed"}}}},"/api/shares/validate":{"get":{"tags":["Sharing"],"operationId":"validate_share_token","parameters":[{"name":"token","in":"query","description":"Share token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareDocumentResponse"}}}}}}},"/api/shares/{token}":{"delete":{"tags":["Sharing"],"operationId":"delete_share","parameters":[{"name":"token","in":"path","description":"Share token","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Share link deleted"}}}},"/api/tags":{"get":{"tags":["Tags"],"operationId":"list_tags","parameters":[{"name":"q","in":"query","description":"Filter contains","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TagItem"}}}}}}}},"/api/workspace-invitations/{token}/accept":{"post":{"tags":["Workspaces"],"operationId":"accept_invitation","parameters":[{"name":"token","in":"path","description":"Invitation token","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":""}}}},"/api/workspaces":{"get":{"tags":["Workspaces"],"operationId":"list_workspaces","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}}},"post":{"tags":["Workspaces"],"operationId":"create_workspace","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}}},"/api/workspaces/{id}":{"get":{"tags":["Workspaces"],"operationId":"get_workspace_detail","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}},"put":{"tags":["Workspaces"],"operationId":"update_workspace","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponse"}}}}}},"delete":{"tags":["Workspaces"],"operationId":"delete_workspace","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}}},"/api/workspaces/{id}/download":{"get":{"tags":["Workspaces"],"operationId":"download_workspace_archive","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","description":"Download format (archive only)","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/DownloadFormat"}],"nullable":true}}],"responses":{"200":{"description":"Workspace download","content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/DocumentDownloadBinary"}}}},"401":{"description":"Unauthorized"},"404":{"description":"Workspace not found"}}}},"/api/workspaces/{id}/invitations":{"get":{"tags":["Workspaces"],"operationId":"list_invitations","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceInvitationResponse"}}}}}}},"post":{"tags":["Workspaces"],"operationId":"create_invitation","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceInvitationRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceInvitationResponse"}}}}}}},"/api/workspaces/{id}/invitations/{invitation_id}":{"delete":{"tags":["Workspaces"],"operationId":"revoke_invitation","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"invitation_id","in":"path","description":"Invitation ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceInvitationResponse"}}}}}}},"/api/workspaces/{id}/members":{"get":{"tags":["Workspaces"],"operationId":"list_members","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMemberResponse"}}}}}}}},"/api/workspaces/{id}/members/{user_id}":{"delete":{"tags":["Workspaces"],"operationId":"remove_member","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"user_id","in":"path","description":"Target user ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Workspaces"],"operationId":"update_member_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"user_id","in":"path","description":"Target user ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberRoleRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMemberResponse"}}}}}}},"/api/workspaces/{id}/permissions":{"get":{"tags":["Workspaces"],"operationId":"get_workspace_permissions","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspacePermissionsResponse"}}}}}}},"/api/workspaces/{id}/roles":{"get":{"tags":["Workspaces"],"operationId":"list_roles","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceRoleResponse"}}}}}}},"post":{"tags":["Workspaces"],"operationId":"create_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRoleRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponse"}}}}}}},"/api/workspaces/{id}/roles/{role_id}":{"delete":{"tags":["Workspaces"],"operationId":"delete_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"role_id","in":"path","description":"Role ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""}}},"patch":{"tags":["Workspaces"],"operationId":"update_role","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"role_id","in":"path","description":"Role ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRoleRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponse"}}}}}}},"/api/workspaces/{id}/switch":{"post":{"tags":["Workspaces"],"operationId":"switch_workspace","parameters":[{"name":"id","in":"path","description":"Workspace ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchWorkspaceResponse"}}}}}}},"/api/yjs/{id}":{"get":{"tags":["Realtime"],"operationId":"axum_ws_entry","parameters":[{"name":"id","in":"path","description":"Document ID (UUID)","required":true,"schema":{"type":"string"}},{"name":"token","in":"query","description":"JWT or share token","required":false,"schema":{"type":"string","nullable":true}},{"name":"Authorization","in":"header","description":"Bearer token (JWT or share token)","required":false,"schema":{"type":"string","nullable":true}}],"responses":{"101":{"description":"Switching Protocols (WebSocket upgrade)"},"401":{"description":"Unauthorized"}}}}},"components":{"schemas":{"ActiveShareItem":{"type":"object","required":["id","token","permission","created_at","document_id","document_title","document_type","url"],"properties":{"created_at":{"type":"string","format":"date-time"},"document_id":{"type":"string","format":"uuid"},"document_title":{"type":"string"},"document_type":{"type":"string","description":"'document' or 'folder'"},"expires_at":{"type":"string","format":"date-time","nullable":true},"id":{"type":"string","format":"uuid"},"parent_share_id":{"type":"string","format":"uuid","nullable":true},"permission":{"type":"string"},"token":{"type":"string"},"url":{"type":"string"}}},"AddPatternsRequest":{"type":"object","required":["patterns"],"properties":{"patterns":{"type":"array","items":{"type":"string"}}}},"ApiTokenCreateRequest":{"type":"object","properties":{"name":{"type":"string","example":"Deploy token","nullable":true}}},"ApiTokenCreateResponse":{"type":"object","required":["id","name","created_at","token"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"token":{"type":"string"}}},"ApiTokenItem":{"type":"object","required":["id","name","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string","format":"date-time","nullable":true},"name":{"type":"string"},"revoked_at":{"type":"string","format":"date-time","nullable":true}}},"ApplicableShareItem":{"type":"object","required":["token","permission","scope","excluded"],"properties":{"excluded":{"type":"boolean"},"permission":{"type":"string"},"scope":{"type":"string","description":"'document' or 'folder'"},"token":{"type":"string"}}},"AuthProviderInfoResponse":{"type":"object","required":["id","requires_state","client_ids"],"properties":{"authorization_url":{"type":"string","nullable":true},"client_ids":{"type":"array","items":{"type":"string"}},"id":{"type":"string"},"name":{"type":"string","nullable":true},"redirect_uri":{"type":"string","nullable":true},"requires_state":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}}},"AuthProvidersResponse":{"type":"object","required":["providers"],"properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/AuthProviderInfoResponse"}}}},"BacklinkInfo":{"type":"object","required":["document_id","title","document_type","link_type","link_count"],"properties":{"document_id":{"type":"string"},"document_type":{"type":"string"},"file_path":{"type":"string","nullable":true},"link_count":{"type":"integer","format":"int64"},"link_text":{"type":"string","nullable":true},"link_type":{"type":"string"},"title":{"type":"string"}}},"BacklinksResponse":{"type":"object","required":["backlinks","total_count"],"properties":{"backlinks":{"type":"array","items":{"$ref":"#/components/schemas/BacklinkInfo"}},"total_count":{"type":"integer","minimum":0}}},"CheckIgnoredRequest":{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}},"CreateDocumentRequest":{"type":"object","properties":{"parent_id":{"type":"string","format":"uuid","nullable":true},"title":{"type":"string","nullable":true},"type":{"type":"string","nullable":true}}},"CreateGitConfigRequest":{"type":"object","required":["repository_url","auth_type","auth_data"],"properties":{"auth_data":{},"auth_type":{"type":"string"},"auto_sync":{"type":"boolean","nullable":true},"branch_name":{"type":"string","nullable":true},"repository_url":{"type":"string"}}},"CreateRecordBody":{"type":"object","required":["data"],"properties":{"data":{}}},"CreateShareMountRequest":{"type":"object","required":["token"],"properties":{"parent_folder_id":{"type":"string","format":"uuid","nullable":true},"token":{"type":"string"}}},"CreateShareRequest":{"type":"object","required":["document_id"],"properties":{"document_id":{"type":"string","format":"uuid"},"expires_at":{"type":"string","format":"date-time","nullable":true},"permission":{"type":"string","nullable":true}}},"CreateShareResponse":{"type":"object","required":["token","url"],"properties":{"token":{"type":"string"},"url":{"type":"string"}}},"CreateWorkspaceInvitationRequest":{"type":"object","required":["email","role_kind"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"email":{"type":"string"},"expires_at":{"type":"string","format":"date-time","nullable":true},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"CreateWorkspaceRequest":{"type":"object","required":["name"],"properties":{"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"name":{"type":"string"}}},"CreateWorkspaceRoleRequest":{"type":"object","required":["name","base_role"],"properties":{"base_role":{"type":"string"},"description":{"type":"string","nullable":true},"name":{"type":"string"},"overrides":{"type":"array","items":{"$ref":"#/components/schemas/PermissionOverridePayload"},"nullable":true},"priority":{"type":"integer","format":"int32","nullable":true}}},"Document":{"type":"object","required":["id","owner_id","workspace_id","title","type","created_at","updated_at","slug","desired_path"],"properties":{"archived_at":{"type":"string","format":"date-time","nullable":true},"archived_by":{"type":"string","format":"uuid","nullable":true},"archived_parent_id":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid","nullable":true},"created_by_plugin":{"type":"string","nullable":true},"desired_path":{"type":"string"},"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true},"path":{"type":"string","nullable":true},"slug":{"type":"string"},"title":{"type":"string"},"type":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"workspace_id":{"type":"string","format":"uuid"}}},"DocumentArchiveBinary":{"type":"string","format":"binary"},"DocumentDownloadBinary":{"type":"string","format":"binary"},"DocumentListResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Document"}}}},"DocumentPatchOperationRequest":{"oneOf":[{"type":"object","required":["offset","text","op"],"properties":{"offset":{"type":"integer","minimum":0},"op":{"type":"string","enum":["insert"]},"text":{"type":"string"}}},{"type":"object","required":["offset","length","op"],"properties":{"length":{"type":"integer","minimum":0},"offset":{"type":"integer","minimum":0},"op":{"type":"string","enum":["delete"]}}},{"type":"object","required":["offset","length","text","op"],"properties":{"length":{"type":"integer","minimum":0},"offset":{"type":"integer","minimum":0},"op":{"type":"string","enum":["replace"]},"text":{"type":"string"}}}],"discriminator":{"propertyName":"op"}},"DownloadDocumentQuery":{"type":"object","properties":{"format":{"$ref":"#/components/schemas/DownloadFormat"},"token":{"type":"string","nullable":true}}},"DownloadFormat":{"type":"string","enum":["archive","markdown","html","html5","pdf","docx","latex","beamer","context","man","mediawiki","dokuwiki","textile","org","texinfo","opml","docbook","opendocument","odt","rtf","epub","epub3","fb2","asciidoc","icml","slidy","slideous","dzslides","revealjs","s5","json","plain","commonmark","commonmark_x","markdown_strict","markdown_phpextra","markdown_github","rst","native","haddock"]},"DownloadWorkspaceQuery":{"type":"object","properties":{"format":{"$ref":"#/components/schemas/DownloadFormat"}}},"ExecBody":{"type":"object","properties":{"payload":{"nullable":true}}},"ExecResultResponse":{"type":"object","required":["ok","effects"],"properties":{"data":{"nullable":true},"effects":{"type":"array","items":{}},"error":{"nullable":true},"ok":{"type":"boolean"}}},"GitChangeItem":{"type":"object","required":["path","status"],"properties":{"path":{"type":"string"},"status":{"type":"string"}}},"GitChangesResponse":{"type":"object","required":["files"],"properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/GitChangeItem"}}}},"GitCommitItem":{"type":"object","required":["hash","message","author_name","author_email","time"],"properties":{"author_email":{"type":"string"},"author_name":{"type":"string"},"hash":{"type":"string"},"message":{"type":"string"},"time":{"type":"string","format":"date-time"}}},"GitConfigResponse":{"type":"object","required":["id","repository_url","branch_name","auth_type","auto_sync","created_at","updated_at"],"properties":{"auth_type":{"type":"string"},"auto_sync":{"type":"boolean"},"branch_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"remote_check":{"allOf":[{"$ref":"#/components/schemas/GitRemoteCheckResponse"}],"nullable":true},"repository_url":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"GitHistoryResponse":{"type":"object","required":["commits"],"properties":{"commits":{"type":"array","items":{"$ref":"#/components/schemas/GitCommitItem"}}}},"GitRemoteCheckResponse":{"type":"object","required":["ok","message"],"properties":{"message":{"type":"string"},"ok":{"type":"boolean"},"reason":{"type":"string","nullable":true}}},"GitStatus":{"type":"object","required":["repository_initialized","has_remote","uncommitted_changes","untracked_files","sync_enabled"],"properties":{"current_branch":{"type":"string","nullable":true},"has_remote":{"type":"boolean"},"last_sync":{"type":"string","format":"date-time","nullable":true},"last_sync_commit_hash":{"type":"string","nullable":true},"last_sync_message":{"type":"string","nullable":true},"last_sync_status":{"type":"string","nullable":true},"repository_initialized":{"type":"boolean"},"sync_enabled":{"type":"boolean"},"uncommitted_changes":{"type":"integer","format":"int32","minimum":0},"untracked_files":{"type":"integer","format":"int32","minimum":0}}},"GitSyncRequest":{"type":"object","properties":{"force":{"type":"boolean","nullable":true},"full_scan":{"type":"boolean","nullable":true},"message":{"type":"string","nullable":true},"skip_push":{"type":"boolean","nullable":true}}},"GitSyncResponse":{"type":"object","required":["success","message","files_changed"],"properties":{"commit_hash":{"type":"string","nullable":true},"files_changed":{"type":"integer","format":"int32","minimum":0},"message":{"type":"string"},"success":{"type":"boolean"}}},"HealthResp":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}}},"InstallFromUrlBody":{"type":"object","required":["url"],"properties":{"token":{"type":"string","nullable":true},"url":{"type":"string"}}},"InstallResponse":{"type":"object","required":["id","version"],"properties":{"id":{"type":"string"},"version":{"type":"string"}}},"KvValueBody":{"type":"object","required":["value"],"properties":{"value":{}}},"KvValueResponse":{"type":"object","required":["value"],"properties":{"value":{}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"remember_me":{"type":"boolean"}}},"LoginResponse":{"type":"object","required":["access_token","user"],"properties":{"access_token":{"type":"string"},"user":{"$ref":"#/components/schemas/UserResponse"}}},"ManifestItem":{"type":"object","required":["id","version","scope","mounts","frontend","permissions","config","ui"],"properties":{"author":{"type":"string","nullable":true},"config":{},"frontend":{},"id":{"type":"string"},"mounts":{"type":"array","items":{"type":"string"}},"name":{"type":"string","nullable":true},"permissions":{"type":"array","items":{"type":"string"}},"repository":{"type":"string","nullable":true},"scope":{"type":"string"},"ui":{},"version":{"type":"string"}}},"MaterializeResponse":{"type":"object","required":["created"],"properties":{"created":{"type":"integer","format":"int64"}}},"OAuthLoginRequest":{"type":"object","properties":{"code":{"type":"string","nullable":true},"credential":{"type":"string","nullable":true},"redirect_uri":{"type":"string","nullable":true},"remember_me":{"type":"boolean"},"state":{"type":"string","nullable":true}}},"OAuthStateResponse":{"type":"object","required":["state"],"properties":{"state":{"type":"string"}}},"OutgoingLink":{"type":"object","required":["document_id","title","document_type","link_type"],"properties":{"document_id":{"type":"string"},"document_type":{"type":"string"},"file_path":{"type":"string","nullable":true},"link_text":{"type":"string","nullable":true},"link_type":{"type":"string"},"position_end":{"type":"integer","format":"int32","nullable":true},"position_start":{"type":"integer","format":"int32","nullable":true},"title":{"type":"string"}}},"OutgoingLinksResponse":{"type":"object","required":["links","total_count"],"properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/OutgoingLink"}},"total_count":{"type":"integer","minimum":0}}},"PatchDocumentContentRequest":{"type":"object","required":["operations"],"properties":{"operations":{"type":"array","items":{"$ref":"#/components/schemas/DocumentPatchOperationRequest"}}}},"PermissionOverridePayload":{"type":"object","required":["permission","allowed"],"properties":{"allowed":{"type":"boolean"},"permission":{"type":"string"}}},"PlaceholderItemPayload":{"type":"object","required":["kind","id","code"],"properties":{"code":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"}}},"PublicDocumentSummary":{"type":"object","required":["id","title","updated_at","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"published_at":{"type":"string","format":"date-time"},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"PublishResponse":{"type":"object","required":["slug","public_url"],"properties":{"public_url":{"type":"string"},"slug":{"type":"string"}}},"RecordsResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{}}}},"RefreshResponse":{"type":"object","required":["access_token"],"properties":{"access_token":{"type":"string"}}},"RegisterRequest":{"type":"object","required":["email","name","password"],"properties":{"email":{"type":"string"},"name":{"type":"string"},"password":{"type":"string"}}},"RenderManyRequest":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RenderRequest"}}}},"RenderManyResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RenderResponseBody"}}}},"RenderOptionsPayload":{"type":"object","properties":{"absolute_attachments":{"type":"boolean","default":null,"nullable":true},"base_origin":{"type":"string","default":null,"nullable":true},"doc_id":{"type":"string","format":"uuid","default":null,"nullable":true},"features":{"type":"array","items":{"type":"string"},"default":null,"nullable":true},"flavor":{"type":"string","default":null,"nullable":true},"hardbreaks":{"type":"boolean","default":null,"nullable":true},"sanitize":{"type":"boolean","default":null,"nullable":true},"theme":{"type":"string","default":null,"nullable":true},"token":{"type":"string","default":null,"nullable":true}}},"RenderRequest":{"type":"object","required":["text"],"properties":{"options":{"$ref":"#/components/schemas/RenderOptionsPayload"},"text":{"type":"string"}}},"RenderResponseBody":{"type":"object","required":["html","hash"],"properties":{"hash":{"type":"string"},"html":{"type":"string"},"placeholders":{"type":"array","items":{"$ref":"#/components/schemas/PlaceholderItemPayload"}}}},"SearchResult":{"type":"object","required":["id","title","document_type","updated_at"],"properties":{"document_type":{"type":"string"},"id":{"type":"string","format":"uuid"},"path":{"type":"string","nullable":true},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"SessionResponse":{"type":"object","required":["id","workspace_id","remember_me","created_at","last_seen_at","expires_at","current"],"properties":{"created_at":{"type":"string","format":"date-time"},"current":{"type":"boolean"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"ip_address":{"type":"string","nullable":true},"last_seen_at":{"type":"string","format":"date-time"},"remember_me":{"type":"boolean"},"user_agent":{"type":"string","nullable":true},"workspace_id":{"type":"string","format":"uuid"}}},"ShareBrowseResponse":{"type":"object","required":["tree"],"properties":{"tree":{"type":"array","items":{"$ref":"#/components/schemas/ShareBrowseTreeItem"}}}},"ShareBrowseTreeItem":{"type":"object","required":["id","title","type","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true},"title":{"type":"string"},"type":{"type":"string","example":"document"},"updated_at":{"type":"string","format":"date-time"}}},"ShareDocumentResponse":{"type":"object","required":["id","title","permission"],"properties":{"content":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"permission":{"type":"string"},"title":{"type":"string"}}},"ShareItem":{"type":"object","required":["id","token","permission","url","scope"],"properties":{"expires_at":{"type":"string","format":"date-time","nullable":true},"id":{"type":"string","format":"uuid"},"parent_share_id":{"type":"string","format":"uuid","description":"If present, this document share was materialized from a folder share","nullable":true},"permission":{"type":"string"},"scope":{"type":"string","description":"document | folder"},"token":{"type":"string"},"url":{"type":"string"}}},"ShareMountItem":{"type":"object","required":["id","token","target_document_id","target_document_type","target_title","permission","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"parent_folder_id":{"type":"string","format":"uuid","nullable":true},"permission":{"type":"string"},"target_document_id":{"type":"string","format":"uuid"},"target_document_type":{"type":"string"},"target_title":{"type":"string"},"token":{"type":"string"}}},"SnapshotDiffBaseParam":{"type":"string","enum":["auto","current","previous"]},"SnapshotDiffKind":{"type":"string","enum":["current","snapshot"]},"SnapshotDiffResponse":{"type":"object","required":["base","target","diff"],"properties":{"base":{"$ref":"#/components/schemas/SnapshotDiffSideResponse"},"diff":{"$ref":"#/components/schemas/TextDiffResult"},"target":{"$ref":"#/components/schemas/SnapshotDiffSideResponse"}}},"SnapshotDiffSideResponse":{"type":"object","required":["kind","markdown"],"properties":{"kind":{"$ref":"#/components/schemas/SnapshotDiffKind"},"markdown":{"type":"string"},"snapshot":{"allOf":[{"$ref":"#/components/schemas/SnapshotSummary"}],"nullable":true}}},"SnapshotListResponse":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotSummary"}}}},"SnapshotRestoreResponse":{"type":"object","required":["snapshot"],"properties":{"snapshot":{"$ref":"#/components/schemas/SnapshotSummary"}}},"SnapshotSummary":{"type":"object","required":["id","document_id","label","kind","created_at","byte_size","content_hash"],"properties":{"byte_size":{"type":"integer","format":"int64"},"content_hash":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid","nullable":true},"document_id":{"type":"string","format":"uuid"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"label":{"type":"string"},"notes":{"type":"string","nullable":true}}},"SwitchWorkspaceResponse":{"type":"object","required":["access_token"],"properties":{"access_token":{"type":"string"}}},"TagItem":{"type":"object","required":["name","count"],"properties":{"count":{"type":"integer","format":"int64"},"name":{"type":"string"}}},"TextDiffLine":{"type":"object","required":["line_type","content"],"properties":{"content":{"type":"string"},"line_type":{"$ref":"#/components/schemas/TextDiffLineType"},"new_line_number":{"type":"integer","format":"int32","nullable":true,"minimum":0},"old_line_number":{"type":"integer","format":"int32","nullable":true,"minimum":0}}},"TextDiffLineType":{"type":"string","enum":["added","deleted","context"]},"TextDiffResult":{"type":"object","required":["file_path","diff_lines"],"properties":{"diff_lines":{"type":"array","items":{"$ref":"#/components/schemas/TextDiffLine"}},"file_path":{"type":"string"},"new_content":{"type":"string","nullable":true},"old_content":{"type":"string","nullable":true}}},"UninstallBody":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}},"UpdateDocumentContentRequest":{"type":"object","required":["content"],"properties":{"content":{"type":"string"}}},"UpdateDocumentRequest":{"type":"object","properties":{"parent_id":{"type":"string","nullable":true},"title":{"type":"string","nullable":true}}},"UpdateGitConfigRequest":{"type":"object","properties":{"auth_data":{"nullable":true},"auth_type":{"type":"string","nullable":true},"auto_sync":{"type":"boolean","nullable":true},"branch_name":{"type":"string","nullable":true},"repository_url":{"type":"string","nullable":true}}},"UpdateMemberRoleRequest":{"type":"object","required":["role_kind"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"UpdateRecordBody":{"type":"object","required":["patch"],"properties":{"patch":{}}},"UpdateUserShortcutRequest":{"type":"object","properties":{"bindings":{"type":"object"},"leader_key":{"type":"string","example":"","nullable":true}}},"UpdateWorkspaceRequest":{"type":"object","properties":{"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}}},"UpdateWorkspaceRoleRequest":{"type":"object","properties":{"base_role":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"overrides":{"type":"array","items":{"$ref":"#/components/schemas/PermissionOverridePayload"},"nullable":true},"priority":{"type":"integer","format":"int32","nullable":true}}},"UploadFileMultipart":{"type":"object","required":["file","document_id"],"properties":{"document_id":{"type":"string","format":"uuid","description":"Target document ID"},"file":{"type":"string","format":"binary","description":"File to upload"}}},"UploadFileResponse":{"type":"object","required":["id","url","filename","size"],"properties":{"content_type":{"type":"string","nullable":true},"filename":{"type":"string"},"id":{"type":"string","format":"uuid"},"size":{"type":"integer","format":"int64"},"url":{"type":"string"}}},"UserResponse":{"type":"object","required":["id","email","name","workspaces"],"properties":{"active_workspace":{"allOf":[{"$ref":"#/components/schemas/WorkspaceMembershipResponse"}],"nullable":true},"active_workspace_id":{"type":"string","format":"uuid","nullable":true},"active_workspace_permissions":{"type":"array","items":{"type":"string"}},"email":{"type":"string"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMembershipResponse"}}}},"UserShortcutResponse":{"type":"object","required":["bindings"],"properties":{"bindings":{"type":"object"},"leader_key":{"type":"string","example":"","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"WorkspaceInvitationResponse":{"type":"object","required":["id","workspace_id","email","role_kind","invited_by","token","created_at"],"properties":{"accepted_at":{"type":"string","format":"date-time","nullable":true},"accepted_by":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"},"custom_role_id":{"type":"string","format":"uuid","nullable":true},"email":{"type":"string"},"expires_at":{"type":"string","format":"date-time","nullable":true},"id":{"type":"string","format":"uuid"},"invited_by":{"type":"string","format":"uuid"},"revoked_at":{"type":"string","format":"date-time","nullable":true},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true},"token":{"type":"string"},"workspace_id":{"type":"string","format":"uuid"}}},"WorkspaceMemberResponse":{"type":"object","required":["workspace_id","user_id","email","name","role_kind","is_default"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"email":{"type":"string"},"is_default":{"type":"boolean"},"name":{"type":"string"},"role_kind":{"type":"string"},"system_role":{"type":"string","nullable":true},"user_id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"}}},"WorkspaceMembershipResponse":{"type":"object","required":["id","name","slug","is_personal","role_kind","is_default"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"is_default":{"type":"boolean"},"is_personal":{"type":"boolean"},"name":{"type":"string"},"role_kind":{"type":"string"},"slug":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"WorkspacePermissionsResponse":{"type":"object","required":["workspace_id","permissions"],"properties":{"permissions":{"type":"array","items":{"type":"string"}},"workspace_id":{"type":"string","format":"uuid"}}},"WorkspaceResponse":{"type":"object","required":["id","name","slug","is_personal","role_kind","is_default"],"properties":{"custom_role_id":{"type":"string","format":"uuid","nullable":true},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"is_default":{"type":"boolean"},"is_personal":{"type":"boolean"},"name":{"type":"string"},"role_kind":{"type":"string"},"slug":{"type":"string"},"system_role":{"type":"string","nullable":true}}},"WorkspaceRoleResponse":{"type":"object","required":["id","workspace_id","name","base_role","priority","overrides"],"properties":{"base_role":{"type":"string"},"description":{"type":"string","nullable":true},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"overrides":{"type":"array","items":{"$ref":"#/components/schemas/PermissionOverridePayload"}},"priority":{"type":"integer","format":"int32"},"workspace_id":{"type":"string","format":"uuid"}}}}},"tags":[{"name":"Auth","description":"Authentication"},{"name":"Documents","description":"Documents management"},{"name":"Files","description":"File management"},{"name":"Sharing","description":"Document sharing"},{"name":"Public Documents","description":"Public pages"},{"name":"Realtime","description":"Yjs WebSocket endpoint (/yjs/:id)"},{"name":"Git","description":"Git integration"},{"name":"Markdown","description":"Markdown rendering"},{"name":"Plugins","description":"Plugins management & data APIs"},{"name":"Health","description":"System health checks"}]} diff --git a/api/src/application/ports/document_repository.rs b/api/src/application/ports/document_repository.rs index 99605925..5868481d 100644 --- a/api/src/application/ports/document_repository.rs +++ b/api/src/application/ports/document_repository.rs @@ -66,6 +66,7 @@ pub trait DocumentRepository: Send + Sync { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> anyhow::Result; async fn create_for_user_tx( @@ -76,6 +77,7 @@ pub trait DocumentRepository: Send + Sync { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> anyhow::Result; // parent_id: None => not provided; Some(None) => set NULL; Some(Some(uuid)) => set to value diff --git a/api/src/application/services/documents.rs b/api/src/application/services/documents.rs index ba68d3a2..60674ca5 100644 --- a/api/src/application/services/documents.rs +++ b/api/src/application/services/documents.rs @@ -128,6 +128,7 @@ impl DocumentService { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> Result { ensure_can_create(permissions, doc_type)?; if let Some(parent_id) = parent_id { @@ -138,7 +139,15 @@ impl DocumentService { }; let mut tx = self.begin_transaction().await?; let doc = match uc - .execute_tx(&mut tx, workspace_id, actor_id, title, parent_id, doc_type) + .execute_tx( + &mut tx, + workspace_id, + actor_id, + title, + parent_id, + doc_type, + created_by_plugin, + ) .await { Ok(doc) => doc, diff --git a/api/src/application/services/git.rs b/api/src/application/services/git.rs index 91199cef..3a2b75a1 100644 --- a/api/src/application/services/git.rs +++ b/api/src/application/services/git.rs @@ -4,8 +4,8 @@ use uuid::Uuid; use crate::application::dto::diff::TextDiffResult; use crate::application::dto::git::{ - GitChangeItem, GitCommitInfo, GitConfigDto, GitRemoteCheckDto, GitStatusDto, - GitSyncRequestDto, GitSyncResponseDto, GitignoreUpdateDto, UpsertGitConfigInput, + GitChangeItem, GitCommitInfo, GitConfigDto, GitRemoteCheckDto, GitStatusDto, GitSyncRequestDto, + GitSyncResponseDto, GitignoreUpdateDto, UpsertGitConfigInput, }; use crate::application::ports::document_repository::DocumentRepository; use crate::application::ports::files_repository::FilesRepository; @@ -132,23 +132,21 @@ impl GitService { workspace: self.workspace.as_ref(), repo: self.repo.as_ref(), }; - uc.execute(workspace_id, payload) - .await - .map_err(|err| { - let msg_lower = err.to_string().to_lowercase(); - if msg_lower.contains("git_http_auth_redirect") - || msg_lower.contains("too many redirects") - || msg_lower.contains("http (34)") - { - ServiceError::BadRequest("git_auth_redirect") - } else if msg_lower.contains("git_http_not_found") - || msg_lower.contains("status code: 404") - { - ServiceError::BadRequest("git_repo_not_found") - } else { - ServiceError::from(err) - } - }) + uc.execute(workspace_id, payload).await.map_err(|err| { + let msg_lower = err.to_string().to_lowercase(); + if msg_lower.contains("git_http_auth_redirect") + || msg_lower.contains("too many redirects") + || msg_lower.contains("http (34)") + { + ServiceError::BadRequest("git_auth_redirect") + } else if msg_lower.contains("git_http_not_found") + || msg_lower.contains("status code: 404") + { + ServiceError::BadRequest("git_repo_not_found") + } else { + ServiceError::from(err) + } + }) } pub async fn get_changes( diff --git a/api/src/application/services/markdown/mod.rs b/api/src/application/services/markdown/mod.rs index 49c35c2c..3d748018 100644 --- a/api/src/application/services/markdown/mod.rs +++ b/api/src/application/services/markdown/mod.rs @@ -103,9 +103,9 @@ pub fn render( // Provide data-sourcepos for editor<->preview sync c_opts.render.sourcepos = true; // Treat soft line breaks as
; default on for "doc" flavor unless explicitly disabled - let hardbreaks = opts - .hardbreaks - .unwrap_or_else(|| matches!(opts.flavor.as_deref(), Some(f) if f.eq_ignore_ascii_case("doc"))); + let hardbreaks = opts.hardbreaks.unwrap_or_else( + || matches!(opts.flavor.as_deref(), Some(f) if f.eq_ignore_ascii_case("doc")), + ); c_opts.render.hardbreaks = hardbreaks; // Allow HtmlBlock/HtmlInline to pass through; will be sanitized by ammonia afterwards c_opts.render.unsafe_ = true; diff --git a/api/src/application/use_cases/documents/create_document.rs b/api/src/application/use_cases/documents/create_document.rs index 8e4a82d1..f0a828af 100644 --- a/api/src/application/use_cases/documents/create_document.rs +++ b/api/src/application/use_cases/documents/create_document.rs @@ -16,9 +16,17 @@ impl<'a, R: DocumentRepository + ?Sized> CreateDocument<'a, R> { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> anyhow::Result { self.repo - .create_for_user(workspace_id, created_by, title, parent_id, doc_type) + .create_for_user( + workspace_id, + created_by, + title, + parent_id, + doc_type, + created_by_plugin, + ) .await } @@ -30,9 +38,18 @@ impl<'a, R: DocumentRepository + ?Sized> CreateDocument<'a, R> { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> anyhow::Result { self.repo - .create_for_user_tx(tx, workspace_id, created_by, title, parent_id, doc_type) + .create_for_user_tx( + tx, + workspace_id, + created_by, + title, + parent_id, + doc_type, + created_by_plugin, + ) .await } } diff --git a/api/src/application/use_cases/documents/download_document.rs b/api/src/application/use_cases/documents/download_document.rs index 2d6e47c1..7e85d69f 100644 --- a/api/src/application/use_cases/documents/download_document.rs +++ b/api/src/application/use_cases/documents/download_document.rs @@ -206,6 +206,7 @@ where doc_type: "folder".to_string(), created_at: Utc::now(), updated_at: Utc::now(), + created_by_plugin: None, slug: sanitize_filename(workspace_name), desired_path: String::new(), path: None, diff --git a/api/src/application/use_cases/plugins/exec_action.rs b/api/src/application/use_cases/plugins/exec_action.rs index 20572979..5ac3f1bd 100644 --- a/api/src/application/use_cases/plugins/exec_action.rs +++ b/api/src/application/use_cases/plugins/exec_action.rs @@ -150,7 +150,14 @@ where .and_then(|s| Uuid::parse_str(s).ok()); let doc = self .document_repo - .create_for_user(workspace_id, user_id, title, parent_id, doc_type) + .create_for_user( + workspace_id, + user_id, + title, + parent_id, + doc_type, + Some(plugin), + ) .await .map_err(PluginEffectError::from)?; doc_id_created = Some(doc.id); diff --git a/api/src/bin/refmd.rs b/api/src/bin/refmd.rs index ca8f2133..9a05bec2 100644 --- a/api/src/bin/refmd.rs +++ b/api/src/bin/refmd.rs @@ -1,30 +1,34 @@ use std::path::PathBuf; use std::sync::Arc; -use anyhow::{anyhow, bail, ensure, Context, Result}; -use argon2::{password_hash::{PasswordHasher, SaltString}, Argon2}; +use anyhow::{Context, Result, anyhow, bail, ensure}; +use argon2::{ + Argon2, + password_hash::{PasswordHasher, SaltString}, +}; use chrono::{DateTime, Utc}; use clap::{Parser, Subcommand, ValueEnum}; use password_hash::rand_core::OsRng; use sqlx::{Row, types::Json}; use uuid::Uuid; +use api::application::ports::api_token_repository::ApiTokenRepository; use api::application::ports::git_rebuild_job_queue::GitRebuildJobQueue; +use api::application::ports::git_storage::GitStorage; +use api::application::ports::git_workspace::GitWorkspacePort; +use api::application::ports::plugin_asset_store::PluginAssetStore; +use api::application::ports::shares_repository::SharesRepository; use api::application::ports::storage_ingest_queue::{StorageIngestKind, StorageIngestQueue}; use api::application::ports::storage_reconcile_jobs::StorageReconcileJobs; use api::application::ports::user_session_repository::UserSessionRepository; -use api::application::ports::plugin_asset_store::PluginAssetStore; -use api::application::ports::git_workspace::GitWorkspacePort; -use api::application::ports::git_storage::GitStorage; use api::application::services::api_tokens::generate_api_token; -use api::application::ports::api_token_repository::ApiTokenRepository; -use api::application::ports::shares_repository::SharesRepository; use api::application::services::workspaces::WorkspaceService; -use api::application::use_cases::auth::register::{Register, RegisterRequest}; use api::application::use_cases::auth::delete_account::DeleteAccount; +use api::application::use_cases::auth::register::{Register, RegisterRequest}; use api::bootstrap::config::Config; use api::domain::workspaces::permissions::PermissionSet; use api::infrastructure::db; +use api::infrastructure::db::PgPool; use api::infrastructure::db::repositories::api_token_repository_sqlx::SqlxApiTokenRepository; use api::infrastructure::db::repositories::document_repository_sqlx::SqlxDocumentRepository; use api::infrastructure::db::repositories::files_repository_sqlx::SqlxFilesRepository; @@ -34,10 +38,11 @@ use api::infrastructure::db::repositories::shares_repository_sqlx::SqlxSharesRep use api::infrastructure::db::repositories::user_repository_sqlx::SqlxUserRepository; use api::infrastructure::db::repositories::user_session_repository_sqlx::SqlxUserSessionRepository; use api::infrastructure::db::repositories::workspace_repository_sqlx::SqlxWorkspaceRepository; -use api::infrastructure::db::PgPool; use api::infrastructure::git::PgGitRebuildJobQueue; use api::infrastructure::git::storage::{GitStorageDriverConfig, build_git_storage}; -use api::infrastructure::plugins::filesystem_store::{FilesystemPluginStore, PluginExecutionLimits}; +use api::infrastructure::plugins::filesystem_store::{ + FilesystemPluginStore, PluginExecutionLimits, +}; use api::infrastructure::plugins::s3_store::{S3BackedPluginStore, S3PluginStoreConfig}; use api::infrastructure::storage::PgStorageIngestQueue; use api::infrastructure::storage::PgStorageProjectionQueue; @@ -373,7 +378,10 @@ impl CliGitWorkspace { Ok(row.map(|r| (r.get("initialized"), r.get("default_branch")))) } - async fn latest_commit_meta(&self, workspace_id: Uuid) -> anyhow::Result> { + async fn latest_commit_meta( + &self, + workspace_id: Uuid, + ) -> anyhow::Result> { let row = sqlx::query( r#"SELECT commit_id, parent_commit_id, message, author_name, author_email, committed_at, pack_key, file_hash_index @@ -423,7 +431,11 @@ struct DirtyRow { #[async_trait::async_trait] impl GitWorkspacePort for CliGitWorkspace { - async fn ensure_repository(&self, _workspace_id: Uuid, _default_branch: &str) -> anyhow::Result<()> { + async fn ensure_repository( + &self, + _workspace_id: Uuid, + _default_branch: &str, + ) -> anyhow::Result<()> { bail!("ensure_repository not supported in refmd CLI"); } @@ -448,7 +460,10 @@ impl GitWorkspacePort for CliGitWorkspace { Ok(()) } - async fn status(&self, workspace_id: Uuid) -> anyhow::Result { + async fn status( + &self, + workspace_id: Uuid, + ) -> anyhow::Result { let state = self.load_repository_state(workspace_id).await?; let Some((initialized, branch)) = state else { return Ok(api::application::dto::git::GitWorkspaceStatus { @@ -505,7 +520,10 @@ impl GitWorkspacePort for CliGitWorkspace { }) } - async fn list_changes(&self, workspace_id: Uuid) -> anyhow::Result> { + async fn list_changes( + &self, + workspace_id: Uuid, + ) -> anyhow::Result> { if let Some((initialized, _)) = self.load_repository_state(workspace_id).await? { if !initialized { return Ok(Vec::new()); @@ -542,7 +560,10 @@ impl GitWorkspacePort for CliGitWorkspace { Ok(out) } - async fn working_diff(&self, _workspace_id: Uuid) -> anyhow::Result> { + async fn working_diff( + &self, + _workspace_id: Uuid, + ) -> anyhow::Result> { bail!("working_diff not supported in refmd CLI"); } @@ -555,7 +576,10 @@ impl GitWorkspacePort for CliGitWorkspace { bail!("commit_diff not supported in refmd CLI"); } - async fn history(&self, _workspace_id: Uuid) -> anyhow::Result> { + async fn history( + &self, + _workspace_id: Uuid, + ) -> anyhow::Result> { bail!("history not supported in refmd CLI"); } @@ -581,7 +605,9 @@ impl GitWorkspacePort for CliGitWorkspace { } } -fn row_to_commit_meta(row: sqlx::postgres::PgRow) -> anyhow::Result { +fn row_to_commit_meta( + row: sqlx::postgres::PgRow, +) -> anyhow::Result { let commit_id: Vec = row.get("commit_id"); let parent_commit_id: Option> = row.try_get("parent_commit_id").ok(); let message: Option = row.try_get("message").ok(); @@ -589,7 +615,8 @@ fn row_to_commit_meta(row: sqlx::postgres::PgRow) -> anyhow::Result = row.try_get("author_email").ok(); let committed_at: DateTime = row.get("committed_at"); let pack_key: String = row.get("pack_key"); - let file_hash_index: Json> = row.get("file_hash_index"); + let file_hash_index: Json> = + row.get("file_hash_index"); Ok(api::application::ports::git_storage::CommitMeta { commit_id, @@ -672,11 +699,9 @@ async fn main() -> Result<()> { cfg.encryption_key.clone(), ); let git_storage_cfg = match cfg.storage_backend { - api::bootstrap::config::StorageBackend::Filesystem => { - GitStorageDriverConfig::Filesystem { - root: PathBuf::from(cfg.storage_root.clone()), - } - } + api::bootstrap::config::StorageBackend::Filesystem => GitStorageDriverConfig::Filesystem { + root: PathBuf::from(cfg.storage_root.clone()), + }, api::bootstrap::config::StorageBackend::S3 => { let s3_settings = api::infrastructure::git::storage::S3GitStorageConfig { storage_root_prefix: cfg.storage_root.clone(), @@ -737,27 +762,31 @@ async fn handle_users(deps: &Deps, cmd: UserCommand) -> Result<()> { name, password, user_id, - } => create_user( - &deps.user_repo, - deps.workspace_service.as_ref(), - email, - name, - password, - user_id, - ) - .await, + } => { + create_user( + &deps.user_repo, + deps.workspace_service.as_ref(), + email, + name, + password, + user_id, + ) + .await + } UserCommand::SetPassword { user_id, password, revoke_sessions, - } => set_password( - &deps.pool, - &deps.session_repo, - user_id, - password, - revoke_sessions, - ) - .await, + } => { + set_password( + &deps.pool, + &deps.session_repo, + user_id, + password, + revoke_sessions, + ) + .await + } UserCommand::Delete { user_id } => delete_user(deps, user_id).await, UserCommand::Sessions { user_id } => list_sessions(&deps.session_repo, user_id).await, UserCommand::RevokeSessions { user_id } => { @@ -780,17 +809,19 @@ async fn handle_jobs(deps: &Deps, cmd: JobsCommand) -> Result<()> { kind, content_hash, actor_id, - } => enqueue_ingest( - &deps.ingest_queue, - workspace_id, - user_id, - actor_id, - repo_path, - backend, - kind, - content_hash, - ) - .await, + } => { + enqueue_ingest( + &deps.ingest_queue, + workspace_id, + user_id, + actor_id, + repo_path, + backend, + kind, + content_hash, + ) + .await + } }, JobsCommand::Projection { command } => match command { ProjectionCommand::Stats => print_projection_stats(&deps.pool).await, @@ -801,8 +832,13 @@ async fn handle_jobs(deps: &Deps, cmd: JobsCommand) -> Result<()> { workspace_id, scope, } => { - deps.reconcile_jobs.enqueue(workspace_id, scope.trim()).await?; - println!("enqueued reconcile job workspace={workspace_id} scope={}", scope.trim()); + deps.reconcile_jobs + .enqueue(workspace_id, scope.trim()) + .await?; + println!( + "enqueued reconcile job workspace={workspace_id} scope={}", + scope.trim() + ); Ok(()) } }, @@ -813,8 +849,7 @@ async fn handle_jobs(deps: &Deps, cmd: JobsCommand) -> Result<()> { actor_id, } => { let permissions = PermissionSet::all().to_vec(); - deps - .git_rebuild_jobs + deps.git_rebuild_jobs .enqueue(workspace_id, actor_id, &permissions) .await?; println!( @@ -834,7 +869,11 @@ async fn handle_workspaces(deps: &Deps, cmd: WorkspaceCommand) -> Result<()> { list_workspace_members(&deps.pool, workspace_id).await } WorkspaceCommand::Delete { workspace_id } => { - match deps.workspace_service.delete_workspace(workspace_id).await? { + match deps + .workspace_service + .delete_workspace(workspace_id) + .await? + { true => println!("deleted workspace {}", workspace_id), false => println!("workspace {} not found", workspace_id), } @@ -872,8 +911,14 @@ async fn handle_shares(deps: &Deps, cmd: ShareCommand) -> Result<()> { workspace_id, document_id, } => list_shares(&deps.shares_repo, workspace_id, document_id).await, - ShareCommand::Revoke { workspace_id, token } => { - let removed = deps.shares_repo.delete_share(workspace_id, token.trim()).await?; + ShareCommand::Revoke { + workspace_id, + token, + } => { + let removed = deps + .shares_repo + .delete_share(workspace_id, token.trim()) + .await?; if removed { println!("revoked share token {}", token.trim()); } else { @@ -946,7 +991,10 @@ async fn handle_plugins(deps: &Deps, cmd: PluginCommand) -> Result<()> { serde_json::to_string_pretty(&manifest)? ); } - None => println!("manifest not found for plugin={} user={} version={}", plugin_id, user_id, version), + None => println!( + "manifest not found for plugin={} user={} version={}", + plugin_id, user_id, version + ), } Ok(()) } @@ -954,7 +1002,10 @@ async fn handle_plugins(deps: &Deps, cmd: PluginCommand) -> Result<()> { deps.plugin_assets .remove_user_plugin_dir(&user_id, &plugin_id) .await?; - println!("removed plugin data for user {} plugin {}", user_id, plugin_id); + println!( + "removed plugin data for user {} plugin {}", + user_id, plugin_id + ); Ok(()) } } @@ -1012,10 +1063,7 @@ async fn create_user( explicit_user_id: Option, ) -> Result<()> { let normalized_email = email.trim(); - ensure!( - !normalized_email.is_empty(), - "email must not be empty" - ); + ensure!(!normalized_email.is_empty(), "email must not be empty"); ensure!(!password.trim().is_empty(), "password must not be empty"); let user_id = explicit_user_id.unwrap_or_else(Uuid::new_v4); diff --git a/api/src/domain/documents/document.rs b/api/src/domain/documents/document.rs index 118fa15f..c446efd9 100644 --- a/api/src/domain/documents/document.rs +++ b/api/src/domain/documents/document.rs @@ -11,6 +11,7 @@ pub struct Document { pub doc_type: String, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, + pub created_by_plugin: Option, pub slug: String, pub desired_path: String, pub path: Option, diff --git a/api/src/infrastructure/db/repositories/document_repository_sqlx.rs b/api/src/infrastructure/db/repositories/document_repository_sqlx.rs index 8444b5e5..7fe68d4c 100644 --- a/api/src/infrastructure/db/repositories/document_repository_sqlx.rs +++ b/api/src/infrastructure/db/repositories/document_repository_sqlx.rs @@ -47,6 +47,7 @@ impl SqlxDocumentRepository { doc_type: row.get("type"), created_at: row.get("created_at"), updated_at: row.get("updated_at"), + created_by_plugin: row.try_get("created_by_plugin").ok(), slug: row.get("slug"), desired_path: row.get("desired_path"), path: row.try_get("path").ok(), @@ -437,6 +438,7 @@ impl DocumentRepository for SqlxDocumentRepository { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> anyhow::Result { let mut tx = self.pool.begin().await?; let doc = self @@ -447,6 +449,7 @@ impl DocumentRepository for SqlxDocumentRepository { title, parent_id, doc_type, + created_by_plugin, ) .await?; tx.commit().await?; @@ -461,6 +464,7 @@ impl DocumentRepository for SqlxDocumentRepository { title: &str, parent_id: Option, doc_type: &str, + created_by_plugin: Option<&str>, ) -> anyhow::Result { sqlx::query("SAVEPOINT document_create") .execute(tx.as_mut()) @@ -473,8 +477,8 @@ impl DocumentRepository for SqlxDocumentRepository { let repo_path = Self::owner_relative_path(workspace_id, &desired_path); let path_digest = Self::hash_path(&desired_path); let row = sqlx::query( - r#"INSERT INTO documents (title, owner_id, owner_user_id, workspace_id, created_by, parent_id, type, slug, desired_path, path, path_digest) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + r#"INSERT INTO documents (title, owner_id, owner_user_id, workspace_id, created_by, created_by_plugin, parent_id, type, slug, desired_path, path, path_digest) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *"#, ) .bind(title) @@ -482,6 +486,7 @@ impl DocumentRepository for SqlxDocumentRepository { .bind(created_by) .bind(workspace_id) .bind(created_by) + .bind(created_by_plugin) .bind(parent_id) .bind(doc_type) .bind(&slug) diff --git a/api/src/infrastructure/db/repositories/public_repository_sqlx.rs b/api/src/infrastructure/db/repositories/public_repository_sqlx.rs index def3aa45..dd7998a1 100644 --- a/api/src/infrastructure/db/repositories/public_repository_sqlx.rs +++ b/api/src/infrastructure/db/repositories/public_repository_sqlx.rs @@ -140,7 +140,7 @@ impl PublicRepository for SqlxPublicRepository { ) -> anyhow::Result> { let row = sqlx::query( r#"SELECT d.id, d.owner_id, d.owner_user_id, d.workspace_id, d.title, d.parent_id, d.type, d.created_at, d.updated_at, - d.slug, d.desired_path, d.path, d.created_by, + d.slug, d.desired_path, d.path, d.created_by, d.created_by_plugin, d.archived_at, d.archived_by, d.archived_parent_id FROM public_documents p JOIN documents d ON p.document_id = d.id @@ -171,6 +171,7 @@ impl PublicRepository for SqlxPublicRepository { desired_path: r.get("desired_path"), path: r.try_get("path").ok(), created_by: r.try_get("created_by").ok(), + created_by_plugin: r.try_get("created_by_plugin").ok(), archived_at: r.try_get("archived_at").ok(), archived_by: r.try_get("archived_by").ok(), archived_parent_id: r.try_get("archived_parent_id").ok(), diff --git a/api/src/infrastructure/git/workspace.rs b/api/src/infrastructure/git/workspace.rs index bcb17bed..ee3f493c 100644 --- a/api/src/infrastructure/git/workspace.rs +++ b/api/src/infrastructure/git/workspace.rs @@ -1930,7 +1930,8 @@ impl GitWorkspacePort for GitWorkspaceService { Some("auth_required".to_string()), "remote requires authentication or SSO approval".to_string(), ) - } else if lower.contains("git_http_not_found") || lower.contains("status code: 404") { + } else if lower.contains("git_http_not_found") || lower.contains("status code: 404") + { ( Some("repo_not_found".to_string()), "repository URL or branch not found".to_string(), diff --git a/api/src/main.rs b/api/src/main.rs index e9c6e244..18cbddcb 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1011,9 +1011,7 @@ async fn main() -> anyhow::Result<()> { let frontend_origin = if let Some(origin) = cfg.frontend_url.clone() { Some(HeaderValue::from_str(&origin).map_err(|_| { - anyhow::anyhow!( - "FRONTEND_URL must be a valid origin (e.g., https://app.example.com)" - ) + anyhow::anyhow!("FRONTEND_URL must be a valid origin (e.g., https://app.example.com)") })?) } else { None diff --git a/api/src/presentation/http/auth.rs b/api/src/presentation/http/auth.rs index 2d78666c..aaf6fff7 100644 --- a/api/src/presentation/http/auth.rs +++ b/api/src/presentation/http/auth.rs @@ -1146,7 +1146,7 @@ pub async fn revoke_session( })?; let mut response_headers = HeaderMap::new(); if current_session_id == Some(session_id) { - clear_auth_cookies(&mut response_headers, ctx.cfg.session_cookie_secure); + clear_auth_cookies(&mut response_headers, ctx.cfg.session_cookie_secure); } Ok((response_headers, StatusCode::NO_CONTENT)) } diff --git a/api/src/presentation/http/documents.rs b/api/src/presentation/http/documents.rs index ae9eb291..97b3b852 100644 --- a/api/src/presentation/http/documents.rs +++ b/api/src/presentation/http/documents.rs @@ -35,6 +35,8 @@ pub struct Document { pub r#type: String, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub created_by_plugin: Option, pub slug: String, pub desired_path: String, pub path: Option, @@ -54,6 +56,7 @@ fn to_http_document(doc: domain::Document) -> Document { r#type: doc.doc_type, created_at: doc.created_at, updated_at: doc.updated_at, + created_by_plugin: doc.created_by_plugin, slug: doc.slug, desired_path: doc.desired_path, path: doc.path, @@ -328,6 +331,7 @@ pub async fn create_document( &title, req.parent_id, &dtype, + None, ) .await .map_err(map_service_error)?; diff --git a/api/src/presentation/http/git.rs b/api/src/presentation/http/git.rs index 676443d7..386c39ac 100644 --- a/api/src/presentation/http/git.rs +++ b/api/src/presentation/http/git.rs @@ -184,7 +184,10 @@ pub async fn get_config( workspace_scope::ensure_workspace_permission(&ctx, workspace_id, user_id, PERM_GIT_CONFIGURE) .await?; let service = ctx.git_service(); - let resp: Option = service.get_config(workspace_id).await.map_err(map_git_error)?; + let resp: Option = service + .get_config(workspace_id) + .await + .map_err(map_git_error)?; let mut out: Option = resp.map(Into::into); if let Some(ref mut cfg) = out { if let Some(check) = service diff --git a/api/src/presentation/http/public.rs b/api/src/presentation/http/public.rs index af5bc6d5..e51c0ae4 100644 --- a/api/src/presentation/http/public.rs +++ b/api/src/presentation/http/public.rs @@ -211,6 +211,7 @@ pub async fn get_public_by_workspace_and_id( r#type: d.doc_type, created_at: d.created_at, updated_at: d.updated_at, + created_by_plugin: d.created_by_plugin, slug: d.slug, desired_path: d.desired_path, path: d.path, diff --git a/app/src/features/file-tree/model/file-tree-context.tsx b/app/src/features/file-tree/model/file-tree-context.tsx index 4eeeeb32..c0e4d85c 100644 --- a/app/src/features/file-tree/model/file-tree-context.tsx +++ b/app/src/features/file-tree/model/file-tree-context.tsx @@ -53,6 +53,7 @@ type DbDoc = { share_token?: string is_share_mount?: boolean share_mount_id?: string + created_by_plugin?: string | null } type BuildTreeOptions = { @@ -101,6 +102,7 @@ function buildTree(docs: DbDoc[], options?: BuildTreeOptions): DocumentNode[] { shareToken: d.share_token, isShareMount: d.is_share_mount, shareMountId: d.share_mount_id, + createdByPlugin: d.created_by_plugin ?? null, }) const parentId = (useArchivedParent ? d.archived_parent_id : d.parent_id) ?? undefined parentRef.set(d.id, parentId ?? undefined) diff --git a/app/src/features/file-tree/model/types.ts b/app/src/features/file-tree/model/types.ts index 58d3eff2..7e8a7e7e 100644 --- a/app/src/features/file-tree/model/types.ts +++ b/app/src/features/file-tree/model/types.ts @@ -11,4 +11,5 @@ export type DocumentNode = { shareToken?: string isShareMount?: boolean shareMountId?: string + createdByPlugin?: string | null } diff --git a/app/src/features/file-tree/ui/FileNode.tsx b/app/src/features/file-tree/ui/FileNode.tsx index e743e9ed..f80b9d76 100644 --- a/app/src/features/file-tree/ui/FileNode.tsx +++ b/app/src/features/file-tree/ui/FileNode.tsx @@ -199,7 +199,12 @@ export const FileNode = memo(function FileNode({ } }, [renameTarget, node.id, node.title, isEditing, isArchived]) - const kvRules = (pluginRules || []).filter(r => r.identify && r.identify.type === 'kvFlag' && !!r.identify.key) + const pluginOwner = (node.createdByPlugin || '').trim() + + const kvRules = pluginOwner + ? [] // Skip KV fetch when plugin owner is already known + : (pluginRules || []).filter((r) => r.identify && r.identify.type === 'kvFlag' && !!r.identify.key) + const shouldFetchPluginFlags = node.type === 'file' && kvRules.length > 0 && (isRowInView || hasBeenVisible) const kvResults = useQueries({ @@ -243,6 +248,12 @@ export const FileNode = memo(function FileNode({ } let chosenIcon: string | null = null + if (pluginOwner) { + const match = (pluginRules || []).find((rule) => rule.pluginId === pluginOwner && typeof rule.icon === 'string') + if (match?.icon) { + chosenIcon = match.icon + } + } for (let i = 0; i < kvRules.length; i++) { const rule = kvRules[i] let value: any = kvResults[i]?.data diff --git a/app/src/features/secondary-viewer/model/useSecondaryViewerContent.ts b/app/src/features/secondary-viewer/model/useSecondaryViewerContent.ts index 9413eea7..a72ecf36 100644 --- a/app/src/features/secondary-viewer/model/useSecondaryViewerContent.ts +++ b/app/src/features/secondary-viewer/model/useSecondaryViewerContent.ts @@ -70,13 +70,34 @@ const cleanupConnection = useCallback(() => { ;(async () => { try { - const plugin = await resolvePluginForDocument(documentId, shareToken, { source: 'secondary' }) + const meta = await fetchDocumentMeta(documentId, shareToken ?? undefined).catch(() => null) if (disposed) return - if (plugin) { - setPluginMatch(plugin) - setCurrentType('plugin') - setIsInitialLoading(false) - return + + const createdByPlugin = + (meta as any)?.created_by_plugin ?? (meta as any)?.createdByPlugin ?? null + if (createdByPlugin) { + const plugin = await resolvePluginForDocument(documentId, shareToken, { + source: 'secondary', + }) + if (disposed) return + if (plugin) { + setPluginMatch(plugin) + setCurrentType('plugin') + setIsInitialLoading(false) + return + } + } else { + // Try resolve only if no explicit plugin owner to avoid unnecessary work + const plugin = await resolvePluginForDocument(documentId, shareToken, { + source: 'secondary', + }) + if (disposed) return + if (plugin) { + setPluginMatch(plugin) + setCurrentType('plugin') + setIsInitialLoading(false) + return + } } if (documentType === 'scrap') { @@ -86,12 +107,6 @@ const cleanupConnection = useCallback(() => { return } - try { - await fetchDocumentMeta(documentId, shareToken ?? undefined) - } catch { - /* ignore meta fetch failure */ - } - const connection = await createYjsConnection(documentId, { token: shareToken ?? undefined }) if (disposed) { destroyYjsConnection(connection) diff --git a/app/src/routeTree.gen.ts b/app/src/routeTree.gen.ts index eac41f96..08c5dd25 100644 --- a/app/src/routeTree.gen.ts +++ b/app/src/routeTree.gen.ts @@ -13,17 +13,17 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as OgVariantDotpngRouteImport } from './routes/og/$variant[.]png' import { Route as appWorkspacesRouteImport } from './routes/(app)/workspaces' import { Route as appTemporaryRouteImport } from './routes/(app)/temporary' -import { Route as appSettingsIndexRouteImport } from './routes/(app)/settings/index' -import { Route as appSettingsShortcutsRouteImport } from './routes/(app)/settings/shortcuts' -import { Route as appSettingsPluginsRouteImport } from './routes/(app)/settings/plugins' -import { Route as appSettingsVisibilityRouteImport } from './routes/(app)/settings/visibility' -import { Route as appSettingsGitSyncRouteImport } from './routes/(app)/settings/git-sync' import { Route as appProfileRouteImport } from './routes/(app)/profile' import { Route as appDashboardRouteImport } from './routes/(app)/dashboard' +import { Route as appSettingsIndexRouteImport } from './routes/(app)/settings/index' import { Route as shareShareTokenRouteImport } from './routes/(share)/share/$token' import { Route as authAuthSignupRouteImport } from './routes/(auth)/auth/signup' import { Route as authAuthSigninRouteImport } from './routes/(auth)/auth/signin' import { Route as appTemporaryIdRouteImport } from './routes/(app)/temporary/$id' +import { Route as appSettingsVisibilityRouteImport } from './routes/(app)/settings/visibility' +import { Route as appSettingsShortcutsRouteImport } from './routes/(app)/settings/shortcuts' +import { Route as appSettingsPluginsRouteImport } from './routes/(app)/settings/plugins' +import { Route as appSettingsGitSyncRouteImport } from './routes/(app)/settings/git-sync' import { Route as appDocumentIdRouteImport } from './routes/(app)/document/$id' import { Route as publicWSlugIndexRouteImport } from './routes/(public)/w/$slug/index' import { Route as publicUNameIndexRouteImport } from './routes/(public)/u/$name/index' @@ -50,31 +50,6 @@ const appTemporaryRoute = appTemporaryRouteImport.update({ path: '/temporary', getParentRoute: () => rootRouteImport, } as any) -const appSettingsIndexRoute = appSettingsIndexRouteImport.update({ - id: '/(app)/settings', - path: '/settings', - getParentRoute: () => rootRouteImport, -} as any) -const appSettingsShortcutsRoute = appSettingsShortcutsRouteImport.update({ - id: '/(app)/settings/shortcuts', - path: '/settings/shortcuts', - getParentRoute: () => rootRouteImport, -} as any) -const appSettingsPluginsRoute = appSettingsPluginsRouteImport.update({ - id: '/(app)/settings/plugins', - path: '/settings/plugins', - getParentRoute: () => rootRouteImport, -} as any) -const appSettingsVisibilityRoute = appSettingsVisibilityRouteImport.update({ - id: '/(app)/settings/visibility', - path: '/settings/visibility', - getParentRoute: () => rootRouteImport, -} as any) -const appSettingsGitSyncRoute = appSettingsGitSyncRouteImport.update({ - id: '/(app)/settings/git-sync', - path: '/settings/git-sync', - getParentRoute: () => rootRouteImport, -} as any) const appProfileRoute = appProfileRouteImport.update({ id: '/(app)/profile', path: '/profile', @@ -85,6 +60,11 @@ const appDashboardRoute = appDashboardRouteImport.update({ path: '/dashboard', getParentRoute: () => rootRouteImport, } as any) +const appSettingsIndexRoute = appSettingsIndexRouteImport.update({ + id: '/(app)/settings/', + path: '/settings/', + getParentRoute: () => rootRouteImport, +} as any) const shareShareTokenRoute = shareShareTokenRouteImport.update({ id: '/(share)/share/$token', path: '/share/$token', @@ -105,6 +85,26 @@ const appTemporaryIdRoute = appTemporaryIdRouteImport.update({ path: '/$id', getParentRoute: () => appTemporaryRoute, } as any) +const appSettingsVisibilityRoute = appSettingsVisibilityRouteImport.update({ + id: '/(app)/settings/visibility', + path: '/settings/visibility', + getParentRoute: () => rootRouteImport, +} as any) +const appSettingsShortcutsRoute = appSettingsShortcutsRouteImport.update({ + id: '/(app)/settings/shortcuts', + path: '/settings/shortcuts', + getParentRoute: () => rootRouteImport, +} as any) +const appSettingsPluginsRoute = appSettingsPluginsRouteImport.update({ + id: '/(app)/settings/plugins', + path: '/settings/plugins', + getParentRoute: () => rootRouteImport, +} as any) +const appSettingsGitSyncRoute = appSettingsGitSyncRouteImport.update({ + id: '/(app)/settings/git-sync', + path: '/settings/git-sync', + getParentRoute: () => rootRouteImport, +} as any) const appDocumentIdRoute = appDocumentIdRouteImport.update({ id: '/(app)/document/$id', path: '/document/$id', @@ -135,19 +135,19 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/dashboard': typeof appDashboardRoute '/profile': typeof appProfileRoute - '/settings': typeof appSettingsIndexRoute - '/settings/git-sync': typeof appSettingsGitSyncRoute - '/settings/plugins': typeof appSettingsPluginsRoute - '/settings/shortcuts': typeof appSettingsShortcutsRoute - '/settings/visibility': typeof appSettingsVisibilityRoute '/temporary': typeof appTemporaryRouteWithChildren '/workspaces': typeof appWorkspacesRoute '/og/$variant.png': typeof OgVariantDotpngRoute '/document/$id': typeof appDocumentIdRoute + '/settings/git-sync': typeof appSettingsGitSyncRoute + '/settings/plugins': typeof appSettingsPluginsRoute + '/settings/shortcuts': typeof appSettingsShortcutsRoute + '/settings/visibility': typeof appSettingsVisibilityRoute '/temporary/$id': typeof appTemporaryIdRoute '/auth/signin': typeof authAuthSigninRoute '/auth/signup': typeof authAuthSignupRoute '/share/$token': typeof shareShareTokenRoute + '/settings': typeof appSettingsIndexRoute '/u/$name/$id': typeof publicUNameIdRoute '/w/$slug/$id': typeof publicWSlugIdRoute '/u/$name': typeof publicUNameIndexRoute @@ -157,19 +157,19 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/dashboard': typeof appDashboardRoute '/profile': typeof appProfileRoute - '/settings': typeof appSettingsIndexRoute - '/settings/git-sync': typeof appSettingsGitSyncRoute - '/settings/plugins': typeof appSettingsPluginsRoute - '/settings/shortcuts': typeof appSettingsShortcutsRoute - '/settings/visibility': typeof appSettingsVisibilityRoute '/temporary': typeof appTemporaryRouteWithChildren '/workspaces': typeof appWorkspacesRoute '/og/$variant.png': typeof OgVariantDotpngRoute '/document/$id': typeof appDocumentIdRoute + '/settings/git-sync': typeof appSettingsGitSyncRoute + '/settings/plugins': typeof appSettingsPluginsRoute + '/settings/shortcuts': typeof appSettingsShortcutsRoute + '/settings/visibility': typeof appSettingsVisibilityRoute '/temporary/$id': typeof appTemporaryIdRoute '/auth/signin': typeof authAuthSigninRoute '/auth/signup': typeof authAuthSignupRoute '/share/$token': typeof shareShareTokenRoute + '/settings': typeof appSettingsIndexRoute '/u/$name/$id': typeof publicUNameIdRoute '/w/$slug/$id': typeof publicWSlugIdRoute '/u/$name': typeof publicUNameIndexRoute @@ -180,19 +180,19 @@ export interface FileRoutesById { '/': typeof IndexRoute '/(app)/dashboard': typeof appDashboardRoute '/(app)/profile': typeof appProfileRoute - '/(app)/settings': typeof appSettingsIndexRoute - '/(app)/settings/git-sync': typeof appSettingsGitSyncRoute - '/(app)/settings/plugins': typeof appSettingsPluginsRoute - '/(app)/settings/shortcuts': typeof appSettingsShortcutsRoute - '/(app)/settings/visibility': typeof appSettingsVisibilityRoute '/(app)/temporary': typeof appTemporaryRouteWithChildren '/(app)/workspaces': typeof appWorkspacesRoute '/og/$variant.png': typeof OgVariantDotpngRoute '/(app)/document/$id': typeof appDocumentIdRoute + '/(app)/settings/git-sync': typeof appSettingsGitSyncRoute + '/(app)/settings/plugins': typeof appSettingsPluginsRoute + '/(app)/settings/shortcuts': typeof appSettingsShortcutsRoute + '/(app)/settings/visibility': typeof appSettingsVisibilityRoute '/(app)/temporary/$id': typeof appTemporaryIdRoute '/(auth)/auth/signin': typeof authAuthSigninRoute '/(auth)/auth/signup': typeof authAuthSignupRoute '/(share)/share/$token': typeof shareShareTokenRoute + '/(app)/settings/': typeof appSettingsIndexRoute '/(public)/u/$name/$id': typeof publicUNameIdRoute '/(public)/w/$slug/$id': typeof publicWSlugIdRoute '/(public)/u/$name/': typeof publicUNameIndexRoute @@ -204,19 +204,19 @@ export interface FileRouteTypes { | '/' | '/dashboard' | '/profile' - | '/settings' - | '/settings/git-sync' - | '/settings/plugins' - | '/settings/shortcuts' - | '/settings/visibility' | '/temporary' | '/workspaces' | '/og/$variant.png' | '/document/$id' + | '/settings/git-sync' + | '/settings/plugins' + | '/settings/shortcuts' + | '/settings/visibility' | '/temporary/$id' | '/auth/signin' | '/auth/signup' | '/share/$token' + | '/settings' | '/u/$name/$id' | '/w/$slug/$id' | '/u/$name' @@ -226,19 +226,19 @@ export interface FileRouteTypes { | '/' | '/dashboard' | '/profile' - | '/settings' - | '/settings/git-sync' - | '/settings/plugins' - | '/settings/shortcuts' - | '/settings/visibility' | '/temporary' | '/workspaces' | '/og/$variant.png' | '/document/$id' + | '/settings/git-sync' + | '/settings/plugins' + | '/settings/shortcuts' + | '/settings/visibility' | '/temporary/$id' | '/auth/signin' | '/auth/signup' | '/share/$token' + | '/settings' | '/u/$name/$id' | '/w/$slug/$id' | '/u/$name' @@ -248,19 +248,19 @@ export interface FileRouteTypes { | '/' | '/(app)/dashboard' | '/(app)/profile' - | '/(app)/settings' - | '/(app)/settings/git-sync' - | '/(app)/settings/plugins' - | '/(app)/settings/shortcuts' - | '/(app)/settings/visibility' | '/(app)/temporary' | '/(app)/workspaces' | '/og/$variant.png' | '/(app)/document/$id' + | '/(app)/settings/git-sync' + | '/(app)/settings/plugins' + | '/(app)/settings/shortcuts' + | '/(app)/settings/visibility' | '/(app)/temporary/$id' | '/(auth)/auth/signin' | '/(auth)/auth/signup' | '/(share)/share/$token' + | '/(app)/settings/' | '/(public)/u/$name/$id' | '/(public)/w/$slug/$id' | '/(public)/u/$name/' @@ -271,18 +271,18 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute appDashboardRoute: typeof appDashboardRoute appProfileRoute: typeof appProfileRoute - appSettingsIndexRoute: typeof appSettingsIndexRoute - appSettingsGitSyncRoute: typeof appSettingsGitSyncRoute - appSettingsPluginsRoute: typeof appSettingsPluginsRoute - appSettingsShortcutsRoute: typeof appSettingsShortcutsRoute - appSettingsVisibilityRoute: typeof appSettingsVisibilityRoute appTemporaryRoute: typeof appTemporaryRouteWithChildren appWorkspacesRoute: typeof appWorkspacesRoute OgVariantDotpngRoute: typeof OgVariantDotpngRoute appDocumentIdRoute: typeof appDocumentIdRoute + appSettingsGitSyncRoute: typeof appSettingsGitSyncRoute + appSettingsPluginsRoute: typeof appSettingsPluginsRoute + appSettingsShortcutsRoute: typeof appSettingsShortcutsRoute + appSettingsVisibilityRoute: typeof appSettingsVisibilityRoute authAuthSigninRoute: typeof authAuthSigninRoute authAuthSignupRoute: typeof authAuthSignupRoute shareShareTokenRoute: typeof shareShareTokenRoute + appSettingsIndexRoute: typeof appSettingsIndexRoute publicUNameIdRoute: typeof publicUNameIdRoute publicWSlugIdRoute: typeof publicWSlugIdRoute publicUNameIndexRoute: typeof publicUNameIndexRoute @@ -319,41 +319,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof appTemporaryRouteImport parentRoute: typeof rootRouteImport } - '/(app)/settings': { - id: '/(app)/settings' - path: '/settings' - fullPath: '/settings' - preLoaderRoute: typeof appSettingsIndexRouteImport - parentRoute: typeof rootRouteImport - } - '/(app)/settings/git-sync': { - id: '/(app)/settings/git-sync' - path: '/settings/git-sync' - fullPath: '/settings/git-sync' - preLoaderRoute: typeof appSettingsGitSyncRouteImport - parentRoute: typeof rootRouteImport - } - '/(app)/settings/plugins': { - id: '/(app)/settings/plugins' - path: '/settings/plugins' - fullPath: '/settings/plugins' - preLoaderRoute: typeof appSettingsPluginsRouteImport - parentRoute: typeof rootRouteImport - } - '/(app)/settings/shortcuts': { - id: '/(app)/settings/shortcuts' - path: '/settings/shortcuts' - fullPath: '/settings/shortcuts' - preLoaderRoute: typeof appSettingsShortcutsRouteImport - parentRoute: typeof rootRouteImport - } - '/(app)/settings/visibility': { - id: '/(app)/settings/visibility' - path: '/settings/visibility' - fullPath: '/settings/visibility' - preLoaderRoute: typeof appSettingsVisibilityRouteImport - parentRoute: typeof rootRouteImport - } '/(app)/profile': { id: '/(app)/profile' path: '/profile' @@ -368,6 +333,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof appDashboardRouteImport parentRoute: typeof rootRouteImport } + '/(app)/settings/': { + id: '/(app)/settings/' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof appSettingsIndexRouteImport + parentRoute: typeof rootRouteImport + } '/(share)/share/$token': { id: '/(share)/share/$token' path: '/share/$token' @@ -396,6 +368,34 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof appTemporaryIdRouteImport parentRoute: typeof appTemporaryRoute } + '/(app)/settings/visibility': { + id: '/(app)/settings/visibility' + path: '/settings/visibility' + fullPath: '/settings/visibility' + preLoaderRoute: typeof appSettingsVisibilityRouteImport + parentRoute: typeof rootRouteImport + } + '/(app)/settings/shortcuts': { + id: '/(app)/settings/shortcuts' + path: '/settings/shortcuts' + fullPath: '/settings/shortcuts' + preLoaderRoute: typeof appSettingsShortcutsRouteImport + parentRoute: typeof rootRouteImport + } + '/(app)/settings/plugins': { + id: '/(app)/settings/plugins' + path: '/settings/plugins' + fullPath: '/settings/plugins' + preLoaderRoute: typeof appSettingsPluginsRouteImport + parentRoute: typeof rootRouteImport + } + '/(app)/settings/git-sync': { + id: '/(app)/settings/git-sync' + path: '/settings/git-sync' + fullPath: '/settings/git-sync' + preLoaderRoute: typeof appSettingsGitSyncRouteImport + parentRoute: typeof rootRouteImport + } '/(app)/document/$id': { id: '/(app)/document/$id' path: '/document/$id' @@ -450,18 +450,18 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, appDashboardRoute: appDashboardRoute, appProfileRoute: appProfileRoute, - appSettingsIndexRoute: appSettingsIndexRoute, - appSettingsGitSyncRoute: appSettingsGitSyncRoute, - appSettingsPluginsRoute: appSettingsPluginsRoute, - appSettingsShortcutsRoute: appSettingsShortcutsRoute, - appSettingsVisibilityRoute: appSettingsVisibilityRoute, appTemporaryRoute: appTemporaryRouteWithChildren, appWorkspacesRoute: appWorkspacesRoute, OgVariantDotpngRoute: OgVariantDotpngRoute, appDocumentIdRoute: appDocumentIdRoute, + appSettingsGitSyncRoute: appSettingsGitSyncRoute, + appSettingsPluginsRoute: appSettingsPluginsRoute, + appSettingsShortcutsRoute: appSettingsShortcutsRoute, + appSettingsVisibilityRoute: appSettingsVisibilityRoute, authAuthSigninRoute: authAuthSigninRoute, authAuthSignupRoute: authAuthSignupRoute, shareShareTokenRoute: shareShareTokenRoute, + appSettingsIndexRoute: appSettingsIndexRoute, publicUNameIdRoute: publicUNameIdRoute, publicWSlugIdRoute: publicWSlugIdRoute, publicUNameIndexRoute: publicUNameIndexRoute, diff --git a/app/src/routes/(app)/document/$id.tsx b/app/src/routes/(app)/document/$id.tsx index d132a268..c81550f7 100644 --- a/app/src/routes/(app)/document/$id.tsx +++ b/app/src/routes/(app)/document/$id.tsx @@ -47,9 +47,10 @@ export const Route = createFileRoute('/(app)/document/$id')({ try { const meta = await fetchDocumentMeta(params.id, token) const title = typeof meta?.title === 'string' ? meta.title.trim() : '' - return { title, token } satisfies LoaderData + const createdByPlugin = meta?.created_by_plugin ?? undefined + return { title, token, createdByPlugin } satisfies LoaderData } catch { - return { title: '', token } satisfies LoaderData + return { title: '', token, createdByPlugin: undefined } satisfies LoaderData } }, head: ({ loaderData, params }) => { diff --git a/app/src/routes/(app)/settings/index.tsx b/app/src/routes/(app)/settings/index.tsx index afcd7a11..4d905921 100644 --- a/app/src/routes/(app)/settings/index.tsx +++ b/app/src/routes/(app)/settings/index.tsx @@ -6,7 +6,7 @@ import RouteError from '@/widgets/routes/RouteError' import RoutePending from '@/widgets/routes/RoutePending' import SettingsView from '@/widgets/settings/SettingsView' -export const Route = createFileRoute('/(app)/settings')({ +export const Route = createFileRoute('/(app)/settings/')({ ...settingsRouteConfig, pendingComponent: () => , errorComponent: ({ error }) => , diff --git a/app/src/shared/api/client/types.gen.ts b/app/src/shared/api/client/types.gen.ts index 01131be7..16647a71 100644 --- a/app/src/shared/api/client/types.gen.ts +++ b/app/src/shared/api/client/types.gen.ts @@ -144,6 +144,7 @@ export type Document = { archived_parent_id?: (string) | null; created_at: string; created_by?: (string) | null; + created_by_plugin?: (string) | null; desired_path: string; id: string; owner_id: string; diff --git a/app/src/widgets/document/DocumentPage.tsx b/app/src/widgets/document/DocumentPage.tsx index 8a2fced1..c22dca5b 100644 --- a/app/src/widgets/document/DocumentPage.tsx +++ b/app/src/widgets/document/DocumentPage.tsx @@ -29,6 +29,7 @@ type SecondaryViewerType = ReturnType['secondaryDocum export type DocumentLoaderData = { title: string token?: string + createdByPlugin?: string | null } export type SecondaryViewerRendererProps = { @@ -97,6 +98,7 @@ function DocumentClient({ const { documentTitle: realtimeTitle, documentActions, setDocumentActions } = useRealtime() const hasDoc = Boolean(doc) const { redirecting, resolving: pluginResolving } = usePluginDocumentRedirect(id, { + enabled: Boolean(loaderData?.createdByPlugin), navigate: useCallback((to: string) => navigate({ to }), [navigate]), }) const anonIdentity = useMemo(() => { diff --git a/app/src/widgets/routes/PluginFallback.tsx b/app/src/widgets/routes/PluginFallback.tsx index 4d132cf2..3e675a1b 100644 --- a/app/src/widgets/routes/PluginFallback.tsx +++ b/app/src/widgets/routes/PluginFallback.tsx @@ -4,6 +4,8 @@ import React from 'react' import { useRealtime } from '@/shared/contexts/realtime-context' import { useShareToken } from '@/shared/contexts/share-token-context' +import { fetchDocumentMeta } from '@/entities/document' + import { useAuthContext } from '@/features/auth' import { mountRoutePlugin, @@ -94,6 +96,25 @@ export default function PluginFallback() { ;(async () => { try { + // If the path looks like a document route and has no plugin owner hint, + // skip plugin resolution to avoid unnecessary work. + const docIdMatch = path.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) + if (docIdMatch) { + try { + const meta = await fetchDocumentMeta(docIdMatch[0], shareToken ?? undefined) + const createdByPlugin = (meta as any)?.created_by_plugin ?? (meta as any)?.createdByPlugin + if (!createdByPlugin) { + if (!cancelled) { + setError('Not Found') + setManifestLoading(false) + } + return + } + } catch { + /* ignore meta failures and continue to plugin resolution */ + } + } + const match = await resolvePluginForRoute(path, { token: shareToken ?? undefined }) if (cancelled) return if (!match) { From 532fe08f0deb041e25b44ca50ebbbbd0d14c6def Mon Sep 17 00:00:00 2001 From: munenick Date: Fri, 5 Dec 2025 11:35:12 +0900 Subject: [PATCH 2/3] fix: share permission --- ...02604200002_backfill_created_by_plugin.sql | 30 ++++ api/src/application/access/mod.rs | 2 +- .../application/ports/share_access_port.rs | 1 + .../application/ports/shares_repository.rs | 3 +- api/src/application/services/shares.rs | 30 +++- .../use_cases/shares/browse_share.rs | 2 +- .../db/repositories/shares_repository_sqlx.rs | 6 +- api/src/presentation/http/markdown.rs | 28 +++- api/src/presentation/http/plugins.rs | 158 +++++++++--------- .../plugin/hooks/usePluginExecutor.ts | 30 +++- app/src/features/plugins/lib/resolution.ts | 8 +- app/src/features/plugins/lib/runtime.ts | 55 +++++- .../model/usePluginDocumentRedirect.ts | 14 +- app/src/widgets/document/DocumentPage.tsx | 8 +- app/src/widgets/routes/PluginFallback.tsx | 21 --- 15 files changed, 268 insertions(+), 128 deletions(-) create mode 100644 api/migrations/202604200002_backfill_created_by_plugin.sql diff --git a/api/migrations/202604200002_backfill_created_by_plugin.sql b/api/migrations/202604200002_backfill_created_by_plugin.sql new file mode 100644 index 00000000..760d7581 --- /dev/null +++ b/api/migrations/202604200002_backfill_created_by_plugin.sql @@ -0,0 +1,30 @@ +-- Backfill created_by_plugin for existing documents using plugin KV/records activity +WITH candidates AS ( + SELECT scope_id AS doc_id, plugin, MIN(created_at) AS first_seen + FROM ( + SELECT scope_id, plugin, created_at + FROM plugin_kv + WHERE scope = 'doc' AND scope_id IS NOT NULL AND plugin IS NOT NULL AND plugin <> '' + UNION ALL + SELECT scope_id, plugin, created_at + FROM plugin_records + WHERE scope = 'doc' AND plugin IS NOT NULL AND plugin <> '' + ) s + GROUP BY scope_id, plugin +), +chosen AS ( + SELECT doc_id, plugin + FROM ( + SELECT doc_id, + plugin, + first_seen, + ROW_NUMBER() OVER (PARTITION BY doc_id ORDER BY first_seen) AS rn + FROM candidates + ) t + WHERE rn = 1 +) +UPDATE documents d +SET created_by_plugin = c.plugin +FROM chosen c +WHERE d.id = c.doc_id + AND d.created_by_plugin IS NULL; diff --git a/api/src/application/access/mod.rs b/api/src/application/access/mod.rs index d4b731f3..e8d6c9ce 100644 --- a/api/src/application/access/mod.rs +++ b/api/src/application/access/mod.rs @@ -50,7 +50,7 @@ where } Actor::ShareToken(t) => { // Resolve token target and then decide access when document matches token scope - if let Ok(Some((share_id, perm, expires_at, shared_id, shared_type))) = + if let Ok(Some((share_id, perm, expires_at, shared_id, shared_type, _workspace_id))) = shares_repo.resolve_share_by_token(t).await { if access_repo diff --git a/api/src/application/ports/share_access_port.rs b/api/src/application/ports/share_access_port.rs index f1da19f3..fd0e5bc3 100644 --- a/api/src/application/ports/share_access_port.rs +++ b/api/src/application/ports/share_access_port.rs @@ -13,6 +13,7 @@ pub trait ShareAccessPort: Send + Sync { Option>, Uuid, String, + Uuid, )>, >; diff --git a/api/src/application/ports/shares_repository.rs b/api/src/application/ports/shares_repository.rs index 09d152e0..52b54ae9 100644 --- a/api/src/application/ports/shares_repository.rs +++ b/api/src/application/ports/shares_repository.rs @@ -68,8 +68,9 @@ pub trait SharesRepository: Send + Sync { Option>, Uuid, String, + Uuid, )>, - >; // (share_id, permission, expires_at, shared_id, shared_type) + >; // (share_id, permission, expires_at, shared_id, shared_type, workspace_id) async fn list_share_mounts(&self, workspace_id: Uuid) -> anyhow::Result>; diff --git a/api/src/application/services/shares.rs b/api/src/application/services/shares.rs index dc076b13..fedda596 100644 --- a/api/src/application/services/shares.rs +++ b/api/src/application/services/shares.rs @@ -112,6 +112,26 @@ impl ShareService { uc.execute(token).await.map_err(ServiceError::from) } + pub async fn resolve_share_context( + &self, + token: &str, + ) -> Result< + Option<( + Uuid, + String, + Option>, + Uuid, + String, + Uuid, + )>, + ServiceError, + > { + self.repo + .resolve_share_by_token(token) + .await + .map_err(ServiceError::from) + } + pub async fn list_active( &self, workspace_id: Uuid, @@ -168,8 +188,14 @@ impl ShareService { .await .map_err(ServiceError::from)? .ok_or(ServiceError::NotFound)?; - let (_share_id, permission, expires_at, target_document_id, target_document_type) = - resolved; + let ( + _share_id, + permission, + expires_at, + target_document_id, + target_document_type, + _workspace_id, + ) = resolved; if let Some(exp) = expires_at { if exp < chrono::Utc::now() { return Err(ServiceError::NotFound); diff --git a/api/src/application/use_cases/shares/browse_share.rs b/api/src/application/use_cases/shares/browse_share.rs index 571ff59d..eca4d949 100644 --- a/api/src/application/use_cases/shares/browse_share.rs +++ b/api/src/application/use_cases/shares/browse_share.rs @@ -8,7 +8,7 @@ pub struct BrowseShare<'a, R: SharesRepository + ?Sized> { impl<'a, R: SharesRepository + ?Sized> BrowseShare<'a, R> { pub async fn execute(&self, token: &str) -> anyhow::Result> { let row = self.repo.resolve_share_by_token(token).await?; - let (share_id, _perm, expires_at, shared_id, shared_type) = match row { + let (share_id, _perm, expires_at, shared_id, shared_type, _workspace_id) = match row { Some(r) => r, None => return Ok(None), }; diff --git a/api/src/infrastructure/db/repositories/shares_repository_sqlx.rs b/api/src/infrastructure/db/repositories/shares_repository_sqlx.rs index b7579819..c4cd5ec5 100644 --- a/api/src/infrastructure/db/repositories/shares_repository_sqlx.rs +++ b/api/src/infrastructure/db/repositories/shares_repository_sqlx.rs @@ -25,10 +25,11 @@ impl SqlxSharesRepository { Option>, Uuid, String, + Uuid, )>, > { let row = sqlx::query( - r#"SELECT s.id as share_id, s.permission, s.expires_at, d.id as shared_id, d.type as shared_type + r#"SELECT s.id as share_id, s.permission, s.expires_at, d.id as shared_id, d.type as shared_type, d.workspace_id FROM shares s JOIN documents d ON s.document_id = d.id WHERE s.token = $1"#, @@ -43,6 +44,7 @@ impl SqlxSharesRepository { r.try_get("expires_at").ok(), r.get("shared_id"), r.get("shared_type"), + r.get("workspace_id"), ) })) } @@ -364,6 +366,7 @@ impl SharesRepository for SqlxSharesRepository { Option>, Uuid, String, + Uuid, )>, > { self.fetch_share_resolution(token).await @@ -554,6 +557,7 @@ impl ShareAccessPort for SqlxSharesRepository { Option>, Uuid, String, + Uuid, )>, > { self.fetch_share_resolution(token).await diff --git a/api/src/presentation/http/markdown.rs b/api/src/presentation/http/markdown.rs index 5e1f55fc..03353af4 100644 --- a/api/src/presentation/http/markdown.rs +++ b/api/src/presentation/http/markdown.rs @@ -249,17 +249,31 @@ async fn resolve_user_scope_from_inputs( } } if let Some(token) = share_token { + // Share token: resolve its workspace for renderer so plugin manifests can be loaded. if let Some(actor) = auth::resolve_actor_from_token_str(ctx, token).await { - if let access::Actor::User(uid) = actor { - if let Ok(workspaces) = ctx.workspace_service().list_for_user(uid).await { - if workspaces.is_empty() { - return None; + match actor { + access::Actor::User(uid) => { + if let Ok(workspaces) = ctx.workspace_service().list_for_user(uid).await { + if workspaces.is_empty() { + return None; + } + if let Some(default_ws) = workspaces.iter().find(|ws| ws.is_default) { + return Some(default_ws.id); + } + return Some(workspaces[0].id); } - if let Some(default_ws) = workspaces.iter().find(|ws| ws.is_default) { - return Some(default_ws.id); + } + access::Actor::ShareToken(t) => { + if let Ok(Some((_share_id, _perm, exp, _doc_id, _typ, workspace_id))) = + ctx.share_service().resolve_share_context(&t).await + { + if exp.map(|e| e < chrono::Utc::now()).unwrap_or(false) { + return None; + } + return Some(workspace_id); } - return Some(workspaces[0].id); } + _ => {} } } } diff --git a/api/src/presentation/http/plugins.rs b/api/src/presentation/http/plugins.rs index f89eed5b..f6ab77b6 100644 --- a/api/src/presentation/http/plugins.rs +++ b/api/src/presentation/http/plugins.rs @@ -22,7 +22,8 @@ use crate::application::services::plugins::management::{ }; use crate::application::use_cases::plugins::install_from_url::InstallPluginError; use crate::domain::workspaces::permissions::{ - PERM_PLUGIN_INSTALL, PERM_PLUGIN_RUN, PERM_PLUGIN_UNINSTALL, PermissionSet, + PERM_DOC_EDIT, PERM_DOC_VIEW, PERM_PLUGIN_INSTALL, PERM_PLUGIN_RUN, PERM_PLUGIN_UNINSTALL, + PermissionSet, }; use crate::presentation::context::AppContext; use crate::presentation::http::auth::{self, Bearer}; @@ -35,32 +36,80 @@ struct PluginUserContext { workspace_id: Uuid, user_id: Uuid, permissions: PermissionSet, + actor: access::Actor, } async fn resolve_plugin_user_context( ctx: &AppContext, headers: &HeaderMap, bearer_token: &str, - user_id: Uuid, required_permission: Option<&str>, ) -> Result { - let workspace_id = - workspace_scope::resolve_active_workspace_id(ctx, headers, Some(bearer_token), user_id) - .await - .map_err(|_| StatusCode::FORBIDDEN)?; - let permissions = workspace_scope::resolve_workspace_permissions(ctx, workspace_id, user_id) - .await - .map_err(|_| StatusCode::FORBIDDEN)?; - if let Some(permission) = required_permission { - if !permissions.allows(permission) { - return Err(StatusCode::FORBIDDEN); + if let Some(actor) = auth::resolve_actor_from_token_str(ctx, bearer_token).await { + match actor { + access::Actor::User(user_id) => { + let workspace_id = workspace_scope::resolve_active_workspace_id( + ctx, + headers, + Some(bearer_token), + user_id, + ) + .await + .map_err(|_| StatusCode::FORBIDDEN)?; + let permissions = workspace_scope::resolve_workspace_permissions( + ctx, + workspace_id, + user_id, + ) + .await + .map_err(|_| StatusCode::FORBIDDEN)?; + if let Some(permission) = required_permission { + if !permissions.allows(permission) { + return Err(StatusCode::FORBIDDEN); + } + } + return Ok(PluginUserContext { + workspace_id, + user_id, + permissions, + actor: access::Actor::User(user_id), + }); + } + access::Actor::ShareToken(token) => { + let share = ctx + .share_service() + .resolve_share_context(&token) + .await + .map_err(|_| StatusCode::UNAUTHORIZED)? + .ok_or(StatusCode::UNAUTHORIZED)?; + let (_share_id, perm, expires_at, _shared_id, _shared_type, workspace_id) = share; + if let Some(exp) = expires_at { + if exp < chrono::Utc::now() { + return Err(StatusCode::UNAUTHORIZED); + } + } + let mut permissions = PermissionSet::from_slice(&[PERM_PLUGIN_RUN, PERM_DOC_VIEW]); + if perm == "edit" { + permissions.insert(PERM_DOC_EDIT); + } + if let Some(permission) = required_permission { + if !permissions.allows(permission) { + return Err(StatusCode::FORBIDDEN); + } + } + return Ok(PluginUserContext { + workspace_id, + // Share tokens do not map to a user; use workspace_id as a stable placeholder + user_id: workspace_id, + permissions, + actor: access::Actor::ShareToken(token), + }); + } + _ => {} } } - Ok(PluginUserContext { - workspace_id, - user_id, - permissions, - }) + + Err(StatusCode::UNAUTHORIZED) } pub fn routes(ctx: AppContext) -> Router { @@ -178,17 +227,14 @@ pub async fn list_records( ) -> Result, StatusCode> { ensure_valid_plugin_id(&p.plugin)?; let bearer_token = bearer.0; - let sub = auth::validate_bearer_public(&ctx, Bearer(bearer_token.clone())).await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token.as_str(), - user_id, Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor; ctx.authorization() .require_view(&actor, p.doc_id) .await @@ -261,17 +307,14 @@ pub async fn create_record( ) -> Result, StatusCode> { ensure_valid_plugin_id(&p.plugin)?; let bearer_token = bearer.0; - let sub = auth::validate_bearer_public(&ctx, Bearer(bearer_token.clone())).await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token.as_str(), - user_id, Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor.clone(); ctx.authorization() .require_edit(&actor, p.doc_id) .await @@ -332,21 +375,14 @@ pub async fn update_record( ) -> Result, StatusCode> { ensure_valid_plugin_id(&p.plugin)?; let bearer_token_raw = bearer.0; - let sub = crate::presentation::http::auth::validate_bearer_public( - &ctx, - Bearer(bearer_token_raw.clone()), - ) - .await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token_raw.as_str(), - user_id, Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor.clone(); let plugin_data = ctx.plugin_data_service(); // Get record for scope info and docId to enforce edit permission @@ -401,24 +437,17 @@ pub async fn delete_record( bearer: Bearer, headers: HeaderMap, Path(p): Path, -) -> Result { - ensure_valid_plugin_id(&p.plugin)?; - let bearer_token_raw = bearer.0; - let sub = crate::presentation::http::auth::validate_bearer_public( - &ctx, - Bearer(bearer_token_raw.clone()), - ) - .await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; - let plugin_ctx = resolve_plugin_user_context( - &ctx, - &headers, - bearer_token_raw.as_str(), - user_id, + ) -> Result { + ensure_valid_plugin_id(&p.plugin)?; + let bearer_token_raw = bearer.0; + let plugin_ctx = resolve_plugin_user_context( + &ctx, + &headers, + bearer_token_raw.as_str(), Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor.clone(); let plugin_data = ctx.plugin_data_service(); // Get record to authorize let rec = plugin_data @@ -488,17 +517,14 @@ pub async fn get_kv_value( ) -> Result, StatusCode> { ensure_valid_plugin_id(&p.plugin)?; let bearer_token = bearer.0; - let sub = auth::validate_bearer_public(&ctx, Bearer(bearer_token.clone())).await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token.as_str(), - user_id, Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor.clone(); ctx.authorization() .require_view(&actor, p.doc_id) .await @@ -540,17 +566,14 @@ pub async fn put_kv_value( ) -> Result { ensure_valid_plugin_id(&p.plugin)?; let bearer_token = bearer.0; - let sub = auth::validate_bearer_public(&ctx, Bearer(bearer_token.clone())).await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token.as_str(), - user_id, Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor.clone(); ctx.authorization() .require_edit(&actor, p.doc_id) .await @@ -615,10 +638,8 @@ pub async fn get_manifest( headers: HeaderMap, ) -> Result>, StatusCode> { let bearer_token = bearer.0; - let sub = auth::validate_bearer_public(&ctx, Bearer(bearer_token.clone())).await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = - resolve_plugin_user_context(&ctx, &headers, bearer_token.as_str(), user_id, None).await?; + resolve_plugin_user_context(&ctx, &headers, bearer_token.as_str(), None).await?; let manifests = ctx .plugin_management() .manifests_for_workspace(plugin_ctx.workspace_id, plugin_ctx.user_id) @@ -656,17 +677,14 @@ pub async fn exec_action( ) -> Result, StatusCode> { ensure_valid_plugin_id(&plugin)?; let bearer_token = bearer.0; - let sub = auth::validate_bearer_public(&ctx, Bearer(bearer_token.clone())).await?; - let user_id = Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token.as_str(), - user_id, Some(PERM_PLUGIN_RUN), ) .await?; - let actor = access::Actor::User(plugin_ctx.user_id); + let actor = plugin_ctx.actor.clone(); if let Some(payload) = body.payload.as_ref() { if let Some(doc_id) = extract_doc_id(payload) { ctx.authorization() @@ -761,17 +779,10 @@ pub async fn install_from_url( Json(body): Json, ) -> Result, StatusCode> { let bearer_token_raw = bearer.0; - let sub = crate::presentation::http::auth::validate_bearer_public( - &ctx, - Bearer(bearer_token_raw.clone()), - ) - .await?; - let user_id = uuid::Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token_raw.as_str(), - user_id, Some(PERM_PLUGIN_INSTALL), ) .await?; @@ -831,17 +842,10 @@ pub async fn uninstall( Json(body): Json, ) -> Result { let bearer_token_raw = bearer.0; - let sub = crate::presentation::http::auth::validate_bearer_public( - &ctx, - Bearer(bearer_token_raw.clone()), - ) - .await?; - let user_id = uuid::Uuid::parse_str(&sub).map_err(|_| StatusCode::UNAUTHORIZED)?; let plugin_ctx = resolve_plugin_user_context( &ctx, &headers, bearer_token_raw.as_str(), - user_id, Some(PERM_PLUGIN_UNINSTALL), ) .await?; diff --git a/app/src/entities/plugin/hooks/usePluginExecutor.ts b/app/src/entities/plugin/hooks/usePluginExecutor.ts index cc5146e5..4d0d3103 100644 --- a/app/src/entities/plugin/hooks/usePluginExecutor.ts +++ b/app/src/entities/plugin/hooks/usePluginExecutor.ts @@ -107,6 +107,30 @@ export function usePluginExecutor({ [apiOrigin, plugins], ) + const withShareToken = useCallback( + (target: string) => { + const token = typeof shareToken === 'string' && shareToken.trim().length > 0 ? shareToken.trim() : null + if (!target || !token) return target + try { + const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(target) + const base = typeof window !== 'undefined' ? window.location.origin : undefined + const url = isAbsolute ? new URL(target) : new URL(target, base ?? 'http://localhost') + if (isAbsolute && (!base || url.origin !== base)) { + return target + } + if (!url.searchParams.get('token')) { + url.searchParams.set('token', token) + } + return isAbsolute + ? (base && url.origin === base ? `${url.pathname}${url.search}${url.hash}` : url.toString()) + : `${url.pathname}${url.search}${url.hash}` + } catch { + return target + } + }, + [shareToken], + ) + const resolveDocRoute = useCallback( async (docId: string) => { const ordered = [ @@ -128,7 +152,7 @@ export function usePluginExecutor({ if (canOpen && typeof mod.getRoute === 'function') { const route = await mod.getRoute(docId, { token: shareToken, origin: apiOrigin, host }) if (typeof route === 'string' && route) { - return route + return withShareToken(route) } } } @@ -137,9 +161,9 @@ export function usePluginExecutor({ } } const suffix = shareToken ? `?token=${encodeURIComponent(shareToken)}` : '' - return `/document/${docId}${suffix}` + return withShareToken(`/document/${docId}${suffix}`) }, - [apiOrigin, importPluginModule, plugins, shareToken], + [apiOrigin, importPluginModule, plugins, shareToken, withShareToken], ) const runPluginCommand = useCallback( diff --git a/app/src/features/plugins/lib/resolution.ts b/app/src/features/plugins/lib/resolution.ts index 59fd2b9e..7c374115 100644 --- a/app/src/features/plugins/lib/resolution.ts +++ b/app/src/features/plugins/lib/resolution.ts @@ -5,6 +5,7 @@ import { getPluginManifest, getPluginKv } from '@/entities/plugin' import { createPluginHost, + applyShareTokenToRoute, getApiOrigin, loadPluginModule, } from './runtime' @@ -125,6 +126,9 @@ export async function resolvePluginForDocument( } } + const routeWithToken = applyShareTokenToRoute(route, token) + route = routeWithToken.route + let canOpen = true if (typeof mod.canOpen === 'function') { try { @@ -145,10 +149,10 @@ export async function resolvePluginForDocument( if (!canOpen && !locationMatches) continue - let routeToken: string | null = null + let routeToken: string | null = routeWithToken.token try { const url = new URL(route, window.location.origin) - routeToken = url.searchParams.get('token') + routeToken = routeToken ?? url.searchParams.get('token') } catch { /* noop */ } diff --git a/app/src/features/plugins/lib/runtime.ts b/app/src/features/plugins/lib/runtime.ts index d759cf31..b46f56f5 100644 --- a/app/src/features/plugins/lib/runtime.ts +++ b/app/src/features/plugins/lib/runtime.ts @@ -63,6 +63,46 @@ export function getApiOrigin() { return '' } +export function applyShareTokenToRoute( + route: string, + token?: string | null, +): { route: string; token: string | null } { + const shareToken = typeof token === 'string' && token.trim().length > 0 ? token.trim() : null + if (!route || !shareToken) return { route, token: shareToken } + + try { + const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(route) + const baseOrigin = typeof window !== 'undefined' ? window.location.origin : undefined + const url = isAbsolute + ? new URL(route) + : new URL(route, baseOrigin ?? 'http://localhost') + + if (isAbsolute && (!baseOrigin || url.origin !== baseOrigin)) { + const existingExternalToken = url.searchParams.get('token') + const effectiveExternalToken = existingExternalToken && existingExternalToken.trim().length > 0 + ? existingExternalToken.trim() + : shareToken + return { route, token: effectiveExternalToken } + } + + const existingToken = url.searchParams.get('token') + const effectiveToken = existingToken && existingToken.trim().length > 0 + ? existingToken.trim() + : shareToken + if (!existingToken) { + url.searchParams.set('token', effectiveToken) + } + + const normalized = isAbsolute + ? (baseOrigin && url.origin === baseOrigin ? `${url.pathname}${url.search}${url.hash}` : url.toString()) + : `${url.pathname}${url.search}${url.hash}` + + return { route: normalized, token: effectiveToken } + } catch { + return { route, token: shareToken } + } +} + export function extractDocIdFromRoute(route?: string | null) { if (!route) return null const noHash = route.split('#')[0] || '' @@ -93,32 +133,35 @@ export async function createPluginHost(manifest: ManifestItem, ctx: PluginHostCo const resolvedDocId = ctx.docId ?? (fallbackRoute ? extractDocIdFromRoute(fallbackRoute) : null) const resolvedToken = ctx.token ?? (fallbackRoute ? extractQueryParam(fallbackRoute, 'token') : null) const apiOrigin = getApiOrigin() + const withShareToken = (to: string) => applyShareTokenToRoute(to, resolvedToken).route const fallbackNavigate = (to: string) => { if (!to) return + const target = withShareToken(to) const nav = (window as any).router?.navigate if (typeof nav === 'function') { try { - nav({ to }) + nav({ to: target }) return } catch {} } - window.location.href = to + window.location.href = target } const performNavigate = (to: string) => { if (!to) return + const target = withShareToken(to) if (ctx.navigate) { try { - const result = ctx.navigate(to) + const result = ctx.navigate(target) if (result && typeof (result as Promise).catch === 'function') { - ;(result as Promise).catch(() => fallbackNavigate(to)) + ;(result as Promise).catch(() => fallbackNavigate(target)) } return } catch { - fallbackNavigate(to) + fallbackNavigate(target) return } } - fallbackNavigate(to) + fallbackNavigate(target) } const host = { exec: async (action: string, args: any = {}) => { diff --git a/app/src/features/plugins/model/usePluginDocumentRedirect.ts b/app/src/features/plugins/model/usePluginDocumentRedirect.ts index 81170947..cd1df404 100644 --- a/app/src/features/plugins/model/usePluginDocumentRedirect.ts +++ b/app/src/features/plugins/model/usePluginDocumentRedirect.ts @@ -6,6 +6,7 @@ import type { PluginManifestItem } from '@/entities/plugin' import { getPluginManifest } from '@/entities/plugin' import { + applyShareTokenToRoute, createPluginHost, loadPluginModule, } from '@/features/plugins/lib/runtime' @@ -51,6 +52,8 @@ export function usePluginDocumentRedirect(docId: string, options: Options = {}) } const currentRoute = window.location.pathname + window.location.search + window.location.hash + const withShareToken = (target: string) => applyShareTokenToRoute(target, shareToken).route + const candidates = (manifest as PluginManifestItem[]) .map((plugin) => { const entry = (plugin as any)?.frontend?.entry?.trim?.() @@ -71,30 +74,31 @@ export function usePluginDocumentRedirect(docId: string, options: Options = {}) const modules = await Promise.allSettled(candidates.map((c) => c.loader)) const navigateTo = (target: string) => { + const nextTarget = withShareToken(target) if (!target) return const externalNavigate = navigateRef.current if (externalNavigate) { try { - const result = externalNavigate(target) + const result = externalNavigate(nextTarget) if (result && typeof (result as Promise).catch === 'function') { ;(result as Promise).catch(() => { - window.location.href = target + window.location.href = nextTarget }) } return } catch { - window.location.href = target + window.location.href = nextTarget return } } const nav = (window as any).router?.navigate if (typeof nav === 'function') { try { - nav({ to: target }) + nav({ to: nextTarget }) return } catch {} } - window.location.href = target + window.location.href = nextTarget } for (let index = 0; index < candidates.length; index += 1) { diff --git a/app/src/widgets/document/DocumentPage.tsx b/app/src/widgets/document/DocumentPage.tsx index c22dca5b..1f9b9ce4 100644 --- a/app/src/widgets/document/DocumentPage.tsx +++ b/app/src/widgets/document/DocumentPage.tsx @@ -97,8 +97,14 @@ function DocumentClient({ const { status, doc, awareness, isReadOnly, error: realtimeError } = useCollaborativeDocument(id, shareToken) const { documentTitle: realtimeTitle, documentActions, setDocumentActions } = useRealtime() const hasDoc = Boolean(doc) + const pluginRedirectEnabled = + loaderData?.createdByPlugin === undefined + ? true + : loaderData?.createdByPlugin === null + ? true + : Boolean(loaderData?.createdByPlugin) const { redirecting, resolving: pluginResolving } = usePluginDocumentRedirect(id, { - enabled: Boolean(loaderData?.createdByPlugin), + enabled: pluginRedirectEnabled, navigate: useCallback((to: string) => navigate({ to }), [navigate]), }) const anonIdentity = useMemo(() => { diff --git a/app/src/widgets/routes/PluginFallback.tsx b/app/src/widgets/routes/PluginFallback.tsx index 3e675a1b..4d132cf2 100644 --- a/app/src/widgets/routes/PluginFallback.tsx +++ b/app/src/widgets/routes/PluginFallback.tsx @@ -4,8 +4,6 @@ import React from 'react' import { useRealtime } from '@/shared/contexts/realtime-context' import { useShareToken } from '@/shared/contexts/share-token-context' -import { fetchDocumentMeta } from '@/entities/document' - import { useAuthContext } from '@/features/auth' import { mountRoutePlugin, @@ -96,25 +94,6 @@ export default function PluginFallback() { ;(async () => { try { - // If the path looks like a document route and has no plugin owner hint, - // skip plugin resolution to avoid unnecessary work. - const docIdMatch = path.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) - if (docIdMatch) { - try { - const meta = await fetchDocumentMeta(docIdMatch[0], shareToken ?? undefined) - const createdByPlugin = (meta as any)?.created_by_plugin ?? (meta as any)?.createdByPlugin - if (!createdByPlugin) { - if (!cancelled) { - setError('Not Found') - setManifestLoading(false) - } - return - } - } catch { - /* ignore meta failures and continue to plugin resolution */ - } - } - const match = await resolvePluginForRoute(path, { token: shareToken ?? undefined }) if (cancelled) return if (!match) { From 520c4f801237e7a6cc4eb6a67c6b524a92021028 Mon Sep 17 00:00:00 2001 From: munenick Date: Fri, 5 Dec 2025 14:17:54 +0900 Subject: [PATCH 3/3] fix: permission --- api/src/application/services/git_rebuild.rs | 12 ++ .../application/services/plugins/execution.rs | 17 ++- .../use_cases/plugins/exec_action.rs | 120 +++++++++++++++++- api/src/main.rs | 1 + api/src/presentation/http/plugins.rs | 39 +++++- app/src/widgets/document/DocumentPage.tsx | 6 +- 6 files changed, 179 insertions(+), 16 deletions(-) diff --git a/api/src/application/services/git_rebuild.rs b/api/src/application/services/git_rebuild.rs index 4a2c4db1..6b71b6d3 100644 --- a/api/src/application/services/git_rebuild.rs +++ b/api/src/application/services/git_rebuild.rs @@ -340,6 +340,18 @@ mod tests { }) } } + + async fn check_remote( + &self, + _workspace_id: Uuid, + _cfg: &crate::application::ports::git_repository::UserGitCfg, + ) -> anyhow::Result { + Ok(crate::application::dto::git::GitRemoteCheckDto { + ok: true, + message: "ok".into(), + reason: None, + }) + } } struct RecordingJobQueue { diff --git a/api/src/application/services/plugins/execution.rs b/api/src/application/services/plugins/execution.rs index c40180fd..6b954f62 100644 --- a/api/src/application/services/plugins/execution.rs +++ b/api/src/application/services/plugins/execution.rs @@ -14,6 +14,7 @@ pub struct PluginExecutionService { plugin_repo: Arc, document_repo: Arc, runtime: Arc, + authorization: Arc, } impl PluginExecutionService { @@ -21,11 +22,13 @@ impl PluginExecutionService { plugin_repo: Arc, document_repo: Arc, runtime: Arc, + authorization: Arc, ) -> Self { Self { plugin_repo, document_repo, runtime, + authorization, } } @@ -37,13 +40,25 @@ impl PluginExecutionService { plugin: &str, action: &str, payload: Option, + allowed_doc_id: Option, + actor: &crate::application::access::Actor, ) -> Result, ServiceError> { let uc = ExecutePluginAction { runtime: self.runtime.as_ref(), plugin_repo: self.plugin_repo.as_ref(), document_repo: self.document_repo.as_ref(), + authorization: self.authorization.as_ref(), }; - uc.execute(workspace_id, user_id, permissions, plugin, action, payload) + uc.execute( + workspace_id, + user_id, + permissions, + plugin, + action, + payload, + allowed_doc_id, + actor, + ) .await .map_err(ServiceError::from) } diff --git a/api/src/application/use_cases/plugins/exec_action.rs b/api/src/application/use_cases/plugins/exec_action.rs index 5ac3f1bd..3f0ae61c 100644 --- a/api/src/application/use_cases/plugins/exec_action.rs +++ b/api/src/application/use_cases/plugins/exec_action.rs @@ -7,6 +7,7 @@ use crate::application::ports::document_repository::DocumentRepository; use crate::application::ports::plugin_repository::PluginRepository; use crate::application::ports::plugin_runtime::PluginRuntime; use crate::domain::workspaces::permissions::{PERM_DOC_CREATE, PERM_DOC_EDIT, PermissionSet}; +use crate::{application::access, application::services::authorization::AuthorizationService}; const PERMISSION_DOC_WRITE: &str = "doc.write"; @@ -30,6 +31,7 @@ where pub runtime: &'a RT, pub plugin_repo: &'a PR, pub document_repo: &'a DR, + pub authorization: &'a AuthorizationService, } impl<'a, RT, PR, DR> ExecutePluginAction<'a, RT, PR, DR> @@ -46,6 +48,8 @@ where plugin: &str, action: &str, payload: Option, + allowed_doc_id: Option, + actor: &access::Actor, ) -> anyhow::Result> { let payload = payload.unwrap_or(serde_json::Value::Null); let runtime_scope = Some(workspace_id); @@ -73,6 +77,8 @@ where &res.effects, &permissions, workspace_permissions, + allowed_doc_id, + actor, ) .await { @@ -115,6 +121,8 @@ where effects: &[serde_json::Value], permissions: &HashSet, workspace_permissions: &PermissionSet, + allowed_doc_id: Option, + actor: &access::Actor, ) -> Result, PluginEffectError> { let mut doc_id_created: Option = None; let mut passthrough: Vec = Vec::new(); @@ -176,11 +184,20 @@ where .get("value") .cloned() .unwrap_or(serde_json::Value::Null); - let doc_id = effect - .get("docId") + let doc_id = self + .validate_doc_scope( + workspace_id, + effect + .get("docId") .and_then(|v| v.as_str()) .and_then(|s| Uuid::parse_str(s).ok()) - .or(doc_id_created); + .or(doc_id_created), + allowed_doc_id, + doc_id_created, + actor, + true, + ) + .await?; if let Some(did) = doc_id { self.plugin_repo .kv_set(plugin, "doc", Some(did), key, &value) @@ -202,11 +219,20 @@ where .get("data") .cloned() .unwrap_or_else(|| serde_json::json!({})); - let doc_id = effect - .get("docId") + let doc_id = self + .validate_doc_scope( + workspace_id, + effect + .get("docId") .and_then(|v| v.as_str()) .and_then(|s| Uuid::parse_str(s).ok()) - .or(doc_id_created); + .or(doc_id_created), + allowed_doc_id, + doc_id_created, + actor, + true, + ) + .await?; if let Some(did) = doc_id { let _ = self .plugin_repo @@ -227,6 +253,27 @@ where .and_then(|v| v.as_str()) .and_then(|s| Uuid::parse_str(s).ok()) { + if let Some(rec) = self + .plugin_repo + .get_record(record_id) + .await + .map_err(PluginEffectError::from)? + { + if rec.plugin != plugin { + return Err(PluginEffectError::PermissionDenied { + permission: PERM_DOC_EDIT.to_string(), + }); + } + self.validate_doc_scope( + workspace_id, + Some(rec.scope_id), + allowed_doc_id, + doc_id_created, + actor, + true, + ) + .await?; + } let patch = effect .get("patch") .cloned() @@ -250,6 +297,27 @@ where .and_then(|v| v.as_str()) .and_then(|s| Uuid::parse_str(s).ok()) { + if let Some(rec) = self + .plugin_repo + .get_record(record_id) + .await + .map_err(PluginEffectError::from)? + { + if rec.plugin != plugin { + return Err(PluginEffectError::PermissionDenied { + permission: PERM_DOC_EDIT.to_string(), + }); + } + self.validate_doc_scope( + workspace_id, + Some(rec.scope_id), + allowed_doc_id, + doc_id_created, + actor, + true, + ) + .await?; + } let _ = self .plugin_repo .delete_record(record_id) @@ -286,6 +354,46 @@ where Ok(passthrough) } + async fn validate_doc_scope( + &self, + _workspace_id: Uuid, + mut doc_id: Option, + allowed_doc_id: Option, + doc_id_created: Option, + actor: &access::Actor, + require_edit: bool, + ) -> Result, PluginEffectError> { + // When doc_id is omitted, fall back to the explicitly allowed doc for share tokens. + doc_id = doc_id.or(allowed_doc_id); + + let Some(doc_id) = doc_id else { + return Ok(None); + }; + if let Some(allowed) = allowed_doc_id { + if doc_id != allowed { + return Err(PluginEffectError::PermissionDenied { + permission: PERM_DOC_EDIT.to_string(), + }); + } + } + if Some(doc_id) == doc_id_created { + return Ok(Some(doc_id)); + } + let capability = self.authorization.resolve_document(actor, doc_id).await; + let has_access = if require_edit { + capability >= access::Capability::Edit + } else { + capability >= access::Capability::View + }; + if has_access { + Ok(Some(doc_id)) + } else { + Err(PluginEffectError::PermissionDenied { + permission: PERM_DOC_EDIT.to_string(), + }) + } + } + fn ensure_permission( &self, permissions: &HashSet, diff --git a/api/src/main.rs b/api/src/main.rs index 18cbddcb..4ffb75c6 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -815,6 +815,7 @@ async fn main() -> anyhow::Result<()> { plugin_repo.clone(), document_repo.clone(), plugin_runtime.clone(), + authorization_service.clone(), )); let account_service = Arc::new(AccountService::new( user_repo.clone(), diff --git a/api/src/presentation/http/plugins.rs b/api/src/presentation/http/plugins.rs index f6ab77b6..6db692de 100644 --- a/api/src/presentation/http/plugins.rs +++ b/api/src/presentation/http/plugins.rs @@ -685,14 +685,43 @@ pub async fn exec_action( ) .await?; let actor = plugin_ctx.actor.clone(); - if let Some(payload) = body.payload.as_ref() { - if let Some(doc_id) = extract_doc_id(payload) { - ctx.authorization() - .require_edit(&actor, doc_id) + let doc_id_from_payload = body.payload.as_ref().and_then(extract_doc_id); + let doc_id_from_share = if doc_id_from_payload.is_none() { + if let access::Actor::ShareToken(token) = &actor { + ctx.share_service() + .resolve_share_context(token) + .await + .map_err(map_plugin_service_error)? + .and_then(|(_, _, _, shared_id, shared_type, _)| { + if shared_type == "document" { + Some(shared_id) + } else { + None + } + }) + } else { + None + } + } else { + None + }; + let effective_doc_id = doc_id_from_payload.or(doc_id_from_share); + if let Some(doc_id) = effective_doc_id { + let auth = ctx.authorization(); + if let access::Actor::ShareToken(_) = &actor { + auth.require_view(&actor, doc_id) + .await + .map_err(|_| StatusCode::FORBIDDEN)?; + } else { + auth.require_edit(&actor, doc_id) .await .map_err(|_| StatusCode::FORBIDDEN)?; } } + let allowed_doc_id = match &actor { + access::Actor::ShareToken(_) => effective_doc_id, + _ => None, + }; let exec_service = ctx.plugin_execution_service(); match exec_service .execute_action( @@ -702,6 +731,8 @@ pub async fn exec_action( &plugin, &action, body.payload.clone(), + allowed_doc_id, + &actor, ) .await .map_err(map_plugin_service_error)? diff --git a/app/src/widgets/document/DocumentPage.tsx b/app/src/widgets/document/DocumentPage.tsx index 1f9b9ce4..3d5b3028 100644 --- a/app/src/widgets/document/DocumentPage.tsx +++ b/app/src/widgets/document/DocumentPage.tsx @@ -98,11 +98,7 @@ function DocumentClient({ const { documentTitle: realtimeTitle, documentActions, setDocumentActions } = useRealtime() const hasDoc = Boolean(doc) const pluginRedirectEnabled = - loaderData?.createdByPlugin === undefined - ? true - : loaderData?.createdByPlugin === null - ? true - : Boolean(loaderData?.createdByPlugin) + loaderData?.createdByPlugin == null ? true : Boolean(loaderData?.createdByPlugin) const { redirecting, resolving: pluginResolving } = usePluginDocumentRedirect(id, { enabled: pluginRedirectEnabled, navigate: useCallback((to: string) => navigate({ to }), [navigate]),