{"openapi":"3.1.0","info":{"title":"Evermore Docs Portal","description":"Customer-facing docs portal with magic link and OIDC auth","version":"0.1.0"},"paths":{"/api/docs/login":{"post":{"tags":["docs"],"summary":"Docs Login","description":"Retired endpoint — domain-based login is no longer supported.\n\nThe docs portal previously authenticated external users by email domain\nlookup. That flow was retired in favour of IM-issued magic links that\ncarry explicit scope in the token (see ADR-45cb51db). Requests to this\nendpoint return HTTP 410 Gone.","operationId":"docs_login_api_docs_login_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/docs/magic-link":{"post":{"tags":["docs"],"summary":"Docs Magic Link","description":"Generate a magic link for a customer. Internal users only (ADR-71391314).\n\nBearer-only auth (``get_docs_user_no_cookie``): this is the one state-changing\nendpoint, and ``SameSite=None`` (ADR-50c7cd93 Amendment 2026-06-24) makes the\nsession cookie cross-site-sendable. Requiring the Bearer header — which only\nthe SPA's ``fetch()`` can set — closes the CSRF surface (security review H1).\n\nRequires ``docs.admin`` role (carried in the session JWT from OIDC).\nThe caller supplies customer_name and scope explicitly. ``email`` and\n``client_types`` have been removed from the token schema.","operationId":"docs_magic_link_api_docs_magic_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/docs/verify":{"post":{"tags":["docs"],"summary":"Docs Verify","description":"Verify a magic link token and return a session JWT (ADR-71391314).\n\nScope (customer_name, scope, folder_id, page_id) is read from the token\npayload only — no domain lookup. Tokens containing ``client_types`` are\npre-ADR-71391314 and rejected as revoked; the IM must generate a new link.\nRate-limited per token to prevent brute-force attempts.\n\nInternal session preservation: if the caller already presents a valid\ninternal session JWT in ``Authorization: Bearer``, no new external session\nis minted. The endpoint returns the magic-link payload's destination so the\nfrontend can navigate, but with ``preserve_internal_session=true`` and an\nempty ``token`` field. This prevents IMs from accidentally clobbering their\nfull-access internal session by clicking their own customer-scoped magic\nlinks — see Diane's bug report 2026-05-01.","operationId":"docs_verify_api_docs_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/docs/folders":{"get":{"tags":["docs"],"summary":"Docs Folders","description":"Return the live Confluence folder list. Internal Entra users only.\n\nDerived from label enumeration across all explainer pages. Returns the\ncurrent set of labels and their doc counts. Response cached for 5 minutes\non the Confluence client side (see docs_confluence.py).\n\nExternal magic-link sessions receive 401 — IMs use this endpoint to\ndrive the \"Share folder\" picker; customers never call it directly.","operationId":"docs_folders_api_docs_folders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoldersResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/docs/inventory":{"get":{"tags":["docs"],"summary":"Docs Inventory","description":"Return the document inventory filtered by the user's magic-link scope.\n\nInternal users and scope=all magic-link users see all pages. scope=folder\nusers see only pages whose labels include their folder_id. scope=page\nusers see only the single scoped page.","operationId":"docs_inventory_api_docs_inventory_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/docs/pages/{page_id}":{"get":{"tags":["docs"],"summary":"Docs Page","description":"Return a single document page with content.\n\nInternal users can access any page. Magic-link users are restricted by\nscope: scope=folder users may only access pages in their folder;\nscope=page users may only access their specific page. Out-of-scope\naccess returns 403.","operationId":"docs_page_api_docs_pages__page_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page_id","in":"path","required":true,"schema":{"type":"string","title":"Page Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/docs/images/{page_id}/{filename}":{"get":{"tags":["docs"],"summary":"Docs Image","description":"Return an image attachment for a docs page.\n\nAuth: JWT-based (OIDC-issued for internal, magic-link-issued for external).\nScope enforcement mirrors docs_page — images for out-of-scope pages\nreturn 403.\nReturns the image binary with correct Content-Type and Cache-Control headers.","operationId":"docs_image_api_docs_images__page_id___filename__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page_id","in":"path","required":true,"schema":{"type":"string","title":"Page Id"}},{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/docs/search":{"get":{"tags":["docs"],"summary":"Docs Search","description":"Search documents by keyword (title + body) and/or tag filter.\n\nBoth params are optional and composable:\n- q only → full-text search via Confluence CQL\n- tags only → filter by label (comma-separated)\n- both → full-text within filtered labels\n\nResults are further filtered by the user's magic-link scope.","operationId":"docs_search_api_docs_search_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"tags","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tags"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/docs/external-proxy/_status":{"get":{"tags":["docs"],"summary":"Docs External Proxy Status","description":"Internal-only drift surface for the docs portal's external-link panel.\n\nReturns the SPN-reachability state of every `sodahealth.sharepoint.com`\nURL referenced on the page. Cached per-page-id for ~7 minutes server-side\nso a frontend that opens/closes the panel repeatedly doesn't pound Graph.\n\nAuth: Entra OIDC ONLY. Magic-link customers get 403 unconditionally — the\nset of file URLs on a page is internal-only information.","operationId":"docs_external_proxy_status_api_docs_external_proxy__status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"page_id","in":"query","required":true,"schema":{"type":"string","title":"Page Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalProxyStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/docs/external-proxy/{token}":{"get":{"tags":["docs"],"summary":"Docs External Proxy","description":"Customer-facing file proxy. See module docstring for the full envelope.\n\nThe token in the URL path is server-minted at page-render time by\n`_rewrite_external_sp_links` (WI-4). It binds `(page_id, url)` so a\ncustomer can only fetch URLs that appeared in pages they are entitled\nto see.","operationId":"docs_external_proxy_api_docs_external_proxy__token__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/docs/auth/login":{"get":{"tags":["docs-auth"],"summary":"Docs Oidc Login","description":"Redirect internal users to the Entra OIDC authorize endpoint.\n\nGenerates a signed state token to prevent CSRF, then redirects to Entra's\nauthorize endpoint with the appropriate scopes and redirect URI.","operationId":"docs_oidc_login_docs_auth_login_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/docs/auth/callback":{"get":{"tags":["docs-auth"],"summary":"Docs Oidc Callback","description":"Handle the Entra OIDC callback after user authenticates.\n\nValidates the state parameter, exchanges the authorization code for tokens,\nvalidates the ID token via JWKS, extracts user claims, and issues a session\nJWT. Redirects the user to the frontend with the session JWT.","operationId":"docs_oidc_callback_docs_auth_callback_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/healthz":{"get":{"tags":["health"],"summary":"Healthz","description":"Liveness probe — returns 200 if the process is alive.","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/readyz":{"get":{"tags":["health"],"summary":"Readyz","description":"Readiness probe — returns 200 if the app is ready to serve traffic.","operationId":"readyz_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"tags":["health"],"summary":"Health","description":"Backward-compat alias for /healthz.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/{full_path}":{"get":{"summary":"Spa Catch All","description":"Serve static files or index.html for client-side routes.","operationId":"spa_catch_all__full_path__get","parameters":[{"name":"full_path","in":"path","required":true,"schema":{"type":"string","title":"Full Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ExternalProxyStatusItem":{"properties":{"url":{"type":"string","title":"Url"},"resolves":{"type":"boolean","title":"Resolves"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["url","resolves"],"title":"ExternalProxyStatusItem"},"ExternalProxyStatusResponse":{"properties":{"page_id":{"type":"string","title":"Page Id"},"items":{"items":{"$ref":"#/components/schemas/ExternalProxyStatusItem"},"type":"array","title":"Items"},"cached":{"type":"boolean","title":"Cached","default":false}},"type":"object","required":["page_id","items"],"title":"ExternalProxyStatusResponse"},"FolderItem":{"properties":{"folder_id":{"type":"string","title":"Folder Id"},"label":{"type":"string","title":"Label"},"doc_count":{"type":"integer","title":"Doc Count"}},"type":"object","required":["folder_id","label","doc_count"],"title":"FolderItem"},"FoldersResponse":{"properties":{"folders":{"items":{"$ref":"#/components/schemas/FolderItem"},"type":"array","title":"Folders"}},"type":"object","required":["folders"],"title":"FoldersResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InventoryPage":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"labels":{"items":{"type":"string"},"type":"array","title":"Labels"},"last_updated":{"type":"string","title":"Last Updated","default":""}},"type":"object","required":["id","title","labels"],"title":"InventoryPage"},"InventoryResponse":{"properties":{"pages":{"items":{"$ref":"#/components/schemas/InventoryPage"},"type":"array","title":"Pages"}},"type":"object","required":["pages"],"title":"InventoryResponse"},"MagicLinkRequest":{"properties":{"customer_name":{"type":"string","title":"Customer Name","description":"Customer display name to embed in the token"},"scope":{"type":"string","title":"Scope","description":"Access scope: 'all', 'folder', or 'page'","default":"all"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id","description":"Confluence label for folder scope. Required when scope='folder'."},"page_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page Id","description":"Confluence page ID for page scope. Required when scope='page'."},"expires_in_minutes":{"type":"integer","title":"Expires In Minutes","description":"Link expiry in minutes. Presets: 43200 (30d), 129600 (90d), 5256000 (10y — effectively never). Custom values accepted between 1 min and 5256000 min (10 years); out-of-range values clamp to the 30-day default.","default":43200}},"type":"object","required":["customer_name"],"title":"MagicLinkRequest","description":"Request body for IM-issued magic link generation (ADR-71391314).\n\nScope is carried explicitly on the token. ``email`` and ``client_types``\nhave been removed. ``scope`` must be one of: all, folder, page.\n``folder_id`` is required when scope=folder; ``page_id`` when scope=page."},"MagicLinkResponse":{"properties":{"magic_link_url":{"type":"string","title":"Magic Link Url"},"customer_name":{"type":"string","title":"Customer Name"},"scope":{"type":"string","title":"Scope"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"},"page_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page Id"},"expires_in_minutes":{"type":"integer","title":"Expires In Minutes","default":43200}},"type":"object","required":["magic_link_url","customer_name","scope"],"title":"MagicLinkResponse"},"PageResponse":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"labels":{"items":{"type":"string"},"type":"array","title":"Labels"},"content":{"type":"string","title":"Content"},"last_updated":{"type":"string","title":"Last Updated","default":""}},"type":"object","required":["id","title","labels","content"],"title":"PageResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyRequest":{"properties":{"token":{"type":"string","title":"Token","description":"Magic link token to verify"}},"type":"object","required":["token"],"title":"VerifyRequest"},"VerifyResponse":{"properties":{"token":{"type":"string","title":"Token","default":""},"customer_name":{"type":"string","title":"Customer Name","default":""},"scope":{"type":"string","title":"Scope","default":"all"},"folder_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Folder Id"},"page_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page Id"},"preserve_internal_session":{"type":"boolean","title":"Preserve Internal Session","description":"True when the caller already has a valid internal session JWT and the magic link is being used for navigation only. Frontend should skip writing localStorage and keep the existing internal session.","default":false}},"type":"object","title":"VerifyResponse"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}