{"openapi":"3.1.0","info":{"title":"Nanobase API","version":"1.0.0","description":"Minimal Backend-as-a-Service (BaaS) for indie developers.\nProvides authentication, data storage, file storage, notifications,\nevent tracking, and OAuth integration.\n\n## Authentication\n- **API Key**: Required for all /v1/* endpoints. Pass via X-API-Key header.\n  Format: nb_{project_uuid}_{secret}\n- **JWT Bearer**: Required for /console/* and /admin/* endpoints.\n  Obtained from /console/login or /console/signup.\n- **User Auth**: Optional Bearer token for end-user context within /v1/* routes.\n","contact":{"name":"Nanobase","url":"https://nanobase.dev"}},"servers":[{"url":"https://api.nanobase.cc","description":"Production"},{"url":"http://localhost:8787","description":"Local development"}],"tags":[{"name":"Public","description":"Unauthenticated endpoints"},{"name":"Console","description":"Developer management (JWT auth)"},{"name":"Auth","description":"End-user authentication (API key required)"},{"name":"OAuth","description":"OAuth provider integration (Google, GitHub)"},{"name":"Data","description":"PocketData - collection and record CRUD (API key required)"},{"name":"Storage","description":"File storage with R2 (API key required)"},{"name":"Notico","description":"Email and push notifications (API key required)"},{"name":"Events","description":"Event tracking and analytics (API key required)"},{"name":"Monitor","description":"Event and error logging (API key required)"},{"name":"Admin","description":"Platform owner management (admin JWT required)"}],"paths":{"/":{"get":{"summary":"API root","description":"Returns API metadata, version, and data source attribution.","tags":["Public"],"responses":{"200":{"description":"API information"}}}},"/health":{"get":{"summary":"Health check","description":"Returns service health status, timestamp, and edge region.","tags":["Public"],"responses":{"200":{"description":"Service is healthy"}}}},"/stats/primary":{"get":{"summary":"Primary records for observatory dashboard","tags":["Public"],"responses":{"200":{"description":"Recent project records"}}}},"/console/signup":{"post":{"summary":"Developer signup","tags":["Console"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8}}}}}},"responses":{"201":{"description":"Developer created with JWT"},"409":{"description":"Email already exists"}}}},"/console/login":{"post":{"summary":"Developer login","tags":["Console"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"JWT token and developer info"},"401":{"description":"Invalid credentials"}}}},"/console/me":{"get":{"summary":"Get current developer","tags":["Console"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Developer info"},"401":{"description":"Unauthorized"}}}},"/console/projects":{"get":{"summary":"List developer projects","tags":["Console"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of projects"}}},"post":{"summary":"Create a project","tags":["Console"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Project created"},"403":{"description":"Project limit exceeded"}}}},"/console/projects/{projectId}/keys":{"get":{"summary":"List API keys for a project","tags":["Console"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of API keys"}}},"post":{"summary":"Create an API key","tags":["Console"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"API key created (full key shown only once)"}}}},"/console/projects/{projectId}/keys/{keyId}":{"delete":{"summary":"Delete an API key","tags":["Console"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}},{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"API key deleted"}}}},"/v1/console/projects":{"get":{"summary":"List projects (PPN Hub user)","description":"Lists projects for the developer linked to the authenticated PPN Hub user.\nAuth: PPN Hub API key (X-API-Key) OR X-Internal-API-Key + X-PPN-User-Id\n+ X-PPN-User-Email (server-to-server, console worker).\n","tags":["Console"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"List of projects"},"401":{"description":"Authentication required"}}},"post":{"summary":"Create a project (PPN Hub user)","tags":["Console"],"security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100}}}}}},"responses":{"201":{"description":"Project created"},"400":{"description":"Invalid request body"},"403":{"description":"Project limit exceeded"}}}},"/v1/console/projects/{projectId}":{"delete":{"summary":"Delete a project (PPN Hub user)","tags":["Console"],"security":[{"apiKey":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Project deleted"},"404":{"description":"Project not found or not owned"}}}},"/v1/console/projects/{projectId}/keys":{"get":{"summary":"List API keys for a project (PPN Hub user)","tags":["Console"],"security":[{"apiKey":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of API keys"},"404":{"description":"Project not found"}}},"post":{"summary":"Create an API key (PPN Hub user)","tags":["Console"],"security":[{"apiKey":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100}}}}}},"responses":{"201":{"description":"API key created (full key shown only once)"},"404":{"description":"Project not found"}}}},"/v1/console/keys/{keyId}":{"delete":{"summary":"Delete an API key (PPN Hub user)","tags":["Console"],"security":[{"apiKey":[]}],"parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"API key deleted"},"404":{"description":"Key not found or not owned"}}}},"/v1/auth/signup-email":{"post":{"summary":"End-user email signup","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"metadata":{"type":"object"},"redirectUrl":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"User created with session"},"409":{"description":"Email exists"},"429":{"description":"Rate limited (5/min)"}}}},"/v1/auth/login-email":{"post":{"summary":"End-user email login","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Session with JWT"},"401":{"description":"Invalid credentials"},"429":{"description":"Rate limited (10/min)"}}}},"/v1/auth/me":{"get":{"summary":"Get current user","tags":["Auth"],"security":[{"apiKeyAuth":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"User info"},"401":{"description":"Unauthorized"}}}},"/v1/auth/logout":{"post":{"summary":"Logout — optionally revoke a refresh token","description":"JWT (access token) is stateless and must be deleted client-side.\nIf a refresh token is supplied in the body, it is marked revoked.\n","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token to revoke (optional)"}}}}}},"responses":{"200":{"description":"Logged out"}}}},"/v1/auth/refresh":{"post":{"summary":"Exchange a refresh token for a new access token","description":"Issues a new short-lived access token (JWT) for a valid, unrevoked refresh token.\nMVP keeps the same refresh token (no rotation); it remains valid until logout or 30-day expiry.\n","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["refreshToken"],"properties":{"refreshToken":{"type":"string"}}}}}},"responses":{"200":{"description":"New access token issued"},"400":{"description":"Missing refreshToken"},"401":{"description":"Invalid, revoked, or expired refresh token"},"429":{"description":"Rate limited"}}}},"/v1/auth/magic-link":{"post":{"summary":"Send magic link email","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","redirectUrl"],"properties":{"email":{"type":"string","format":"email"},"redirectUrl":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Magic link sent"},"429":{"description":"Rate limited (3/min)"}}}},"/v1/auth/verify-token":{"post":{"summary":"Verify magic link or email verification token","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","type"],"properties":{"token":{"type":"string"},"type":{"type":"string","enum":["magic_link","email_verification"]}}}}}},"responses":{"200":{"description":"Token verified / session returned"}}}},"/v1/auth/password-reset/request":{"post":{"summary":"Request password reset","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","redirectUrl"],"properties":{"email":{"type":"string"},"redirectUrl":{"type":"string"}}}}}},"responses":{"200":{"description":"Reset email sent (always succeeds for security)"}}}},"/v1/auth/password-reset/confirm":{"post":{"summary":"Confirm password reset","tags":["Auth"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string"},"password":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Password reset"}}}},"/v1/oauth/{provider}/authorize":{"get":{"summary":"Start OAuth flow","tags":["OAuth"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["google","github"]}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string","format":"uri"}}],"responses":{"200":{"description":"Authorization URL and state"}}}},"/v1/oauth/{provider}/callback":{"get":{"summary":"OAuth callback (handled by provider)","tags":["OAuth"],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["google","github"]}}],"responses":{"302":{"description":"Redirect to app with token"}}}},"/v1/oauth/{provider}/token":{"post":{"summary":"Exchange OAuth code for token (SPA flow)","tags":["OAuth"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["google","github"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","state"],"properties":{"code":{"type":"string"},"state":{"type":"string"},"redirect_uri":{"type":"string"}}}}}},"responses":{"200":{"description":"Session with JWT"}}}},"/v1/data/collections":{"get":{"summary":"List all collections","tags":["Data"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"List of collections"}}},"post":{"summary":"Create a collection","tags":["Data"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$"},"schema":{"type":"object"}}}}}},"responses":{"201":{"description":"Collection created"},"409":{"description":"Collection exists"}}}},"/v1/data/collections/{name}":{"delete":{"summary":"Delete a collection and all its records","tags":["Data"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Collection deleted"}}}},"/v1/data/{collection}":{"get":{"summary":"List records in a collection","tags":["Data"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":1000}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"user_only","in":"query","schema":{"type":"string","enum":["true","false"]}},{"name":"where","in":"query","schema":{"type":"string"},"description":"JSON filter object"},{"name":"orderBy","in":"query","schema":{"type":"string"},"description":"JSON sort object e.g. {\"createdAt\":\"desc\"}"}],"responses":{"200":{"description":"List of records"},"404":{"description":"Collection not found"}}},"post":{"summary":"Create a record","tags":["Data"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Arbitrary JSON data"}}}},"responses":{"201":{"description":"Record created"},"403":{"description":"Record limit exceeded"}}}},"/v1/data/{collection}/{id}":{"get":{"summary":"Get a single record","tags":["Data"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Record data"},"404":{"description":"Record not found"}}},"patch":{"summary":"Update a record (merge)","tags":["Data"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Record updated"},"404":{"description":"Record not found"}}},"delete":{"summary":"Delete a record","tags":["Data"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Record deleted"},"404":{"description":"Record not found"}}}},"/v1/storage":{"get":{"summary":"Get storage usage stats","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Storage usage and limits"}}}},"/v1/storage/buckets":{"get":{"summary":"List all buckets","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"List of buckets"}}},"post":{"summary":"Create a bucket","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z][a-zA-Z0-9_-]*$"}}}}}},"responses":{"201":{"description":"Bucket created"}}}},"/v1/storage/buckets/{name}":{"delete":{"summary":"Delete a bucket and all its files","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Bucket deleted"}}}},"/v1/storage/{bucket}":{"get":{"summary":"List files in a bucket","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"bucket","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"List of files"}}},"post":{"summary":"Upload a file","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"bucket","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"metadata":{"type":"string","description":"JSON metadata string"}}}}}},"responses":{"201":{"description":"File uploaded"},"413":{"description":"File too large"}}}},"/v1/storage/{bucket}/{fileId}":{"get":{"summary":"Download or get file metadata","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"bucket","in":"path","required":true,"schema":{"type":"string"}},{"name":"fileId","in":"path","required":true,"schema":{"type":"string"}},{"name":"meta","in":"query","schema":{"type":"string","enum":["true"]},"description":"Set to \"true\" to get JSON metadata instead of file"}],"responses":{"200":{"description":"File content or metadata"},"404":{"description":"File not found"}}},"delete":{"summary":"Delete a file","tags":["Storage"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"bucket","in":"path","required":true,"schema":{"type":"string"}},{"name":"fileId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"File deleted"}}}},"/v1/notico":{"get":{"summary":"List notifications","tags":["Notico"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Notification list"}}}},"/v1/notico/send":{"post":{"summary":"Send a notification","tags":["Notico"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"type":"string","enum":["email","push","line","discord","webhook"]},"recipient":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"}}}}}},"responses":{"201":{"description":"Notification sent"}}}},"/v1/notico/send-template":{"post":{"summary":"Send a templated notification","tags":["Notico"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["recipient","template","link"],"properties":{"recipient":{"type":"string","format":"email"},"template":{"type":"string","enum":["magic-link","verification","password-reset"]},"link":{"type":"string","format":"uri"},"expiresIn":{"type":"string"}}}}}},"responses":{"201":{"description":"Template notification sent"}}}},"/v1/notico/{id}":{"get":{"summary":"Get a notification","tags":["Notico"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Notification details"},"404":{"description":"Not found"}}}},"/v1/events":{"get":{"summary":"List events","tags":["Events"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Event list"}}},"post":{"summary":"Track an event","tags":["Events"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"event":{"type":"string","maxLength":100},"eventType":{"type":"string","default":"custom"},"properties":{"type":"object"},"userId":{"type":"string"}}}}}},"responses":{"201":{"description":"Event tracked"}}}},"/v1/events/stats":{"get":{"summary":"Get event statistics","tags":["Events"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":7}}],"responses":{"200":{"description":"Event statistics by type and date"}}}},"/v1/log/event":{"post":{"summary":"Log an event","tags":["Monitor"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["eventName"],"properties":{"eventName":{"type":"string"},"metadata":{"type":"object"},"timestamp":{"type":"string","format":"date-time"}}}}}},"responses":{"201":{"description":"Event logged"}}}},"/v1/log/error":{"post":{"summary":"Log an error","tags":["Monitor"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"},"stack":{"type":"string"},"context":{"type":"object"}}}}}},"responses":{"201":{"description":"Error logged"}}}},"/v1/log/recent":{"get":{"summary":"Get recent logs","tags":["Monitor"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["event","error"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"Recent logs"}}}},"/v1/log/stats":{"get":{"summary":"Log statistics","tags":["Monitor"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Log stats"}}}},"/admin/stats":{"get":{"summary":"Platform dashboard stats","tags":["Admin"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Platform-wide statistics"}}}},"/admin/developers":{"get":{"summary":"List all developers","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Developer list with stats"}}}},"/admin/developers/{developerId}":{"get":{"summary":"Get developer details with projects","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"developerId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Developer with projects"}}}},"/admin/developers/{developerId}/plan":{"patch":{"summary":"Update developer plan","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"developerId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["free","pro","business"]}}}}}},"responses":{"200":{"description":"Plan updated"}}}},"/admin/projects/{projectId}":{"get":{"summary":"Get project details with users","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Project with users, collections, keys"}}}},"/admin/usage":{"get":{"summary":"Get platform usage metrics","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":30,"maximum":90}}],"responses":{"200":{"description":"Usage metrics over time"}}}},"/admin/projects/{projectId}/users":{"delete":{"summary":"Delete users (all or by email pattern)","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}},{"name":"email_pattern","in":"query","schema":{"type":"string"},"description":"Email pattern for selective deletion"}],"responses":{"200":{"description":"Users deleted"}}}},"/auth/ppn-hub":{"get":{"summary":"Get auth/ppn-hub","description":"Auto-generated stub. Schema/responses/parameters TBD.\nVerify path matches the actual mounted URL (sub-router prefix may be missing).\n","x-stub":true,"responses":{"200":{"description":"Successful response (schema TBD)"}}}},"/auth/ppn-hub/callback":{"get":{"summary":"Get auth/ppn-hub/callback","description":"Auto-generated stub. Schema/responses/parameters TBD.\nVerify path matches the actual mounted URL (sub-router prefix may be missing).\n","x-stub":true,"responses":{"200":{"description":"Successful response (schema TBD)"}}}},"/link-ppn-hub":{"post":{"summary":"Create link-ppn-hub","description":"Auto-generated stub. Schema/responses/parameters TBD.\nVerify path matches the actual mounted URL (sub-router prefix may be missing).\n","x-stub":true,"responses":{"200":{"description":"Successful response (schema TBD)"}}}}},"components":{"securitySchemes":{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Project API key (nb_{project_uuid}_{secret})"},"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT token from login/signup"}},"schemas":{"SuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object"}}},"ErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object"}}}}}}}}