{"openapi":"3.0.0","paths":{"/v1/auth/signup":{"post":{"operationId":"AuthenticationController_signup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"200":{"description":"Token response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponseDto"}}}},"400":{"description":"Validation failed"}},"summary":"Create a new account and sign in","tags":["authentication"]}},"/v1/auth/signin":{"post":{"operationId":"AuthenticationController_signin","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInDto"}}}},"responses":{"200":{"description":"Token response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponseDto"}}}},"400":{"description":"Validation failed"}},"summary":"Sign in with email/username and password","tags":["authentication"]}},"/v1/auth/refresh":{"post":{"operationId":"AuthenticationController_refresh","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshDto"}}}},"responses":{"200":{"description":"Token response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponseDto"}}}},"400":{"description":"Validation failed or invalid token"}},"summary":"Refresh access token using refresh token","tags":["authentication"]}},"/v1/auth/logout":{"post":{"operationId":"AuthenticationController_logout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutDto"}}}},"responses":{"204":{"description":"Logout successful (no content)"},"400":{"description":"Validation failed or invalid token"}},"summary":"Logout by revoking refresh token","tags":["authentication"]}},"/v1/auth/logout-current":{"post":{"operationId":"AuthenticationController_logoutCurrent","parameters":[],"responses":{"204":{"description":"Logout successful (no content)"},"401":{"description":"Missing or invalid access token"}},"security":[{"bearer":[]}],"summary":"Revoke the server-side session tied to the caller’s current access token","tags":["authentication"]}},"/v1/auth/password/request-reset":{"post":{"operationId":"PasswordController_requestPasswordReset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestPasswordResetDto"}}}},"responses":{"201":{"description":"Reset request accepted. If the email matches an account, an email has been queued."}},"summary":"Request a password reset email. The response shape never reveals whether the email matched an account.","tags":["authentication"]}},"/v1/auth/password/reset":{"post":{"operationId":"PasswordController_resetPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"201":{"description":"Password reset; all active sessions revoked."}},"summary":"Reset password using the email-delivered reset token.","tags":["authentication"]}},"/v1/introspect":{"get":{"operationId":"IntrospectController_introspect","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectResponseDto"}}}}},"summary":"Introspect current user with workspace list","tags":["introspect"]}},"/v1/introspect/workspaces/{workspace_id}":{"get":{"operationId":"IntrospectController_introspectWorkspace","parameters":[{"name":"workspace_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectWorkspaceResponseDto"}}}}},"summary":"Introspect caller's membership + effective policy + enabled services in a workspace","tags":["introspect"]}},"/v1/introspect/api-key":{"post":{"operationId":"IntrospectController_introspectApiKey","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntrospectApiKeyResponseDto"}}}}},"summary":"Resolve a Platform API Key (PAK) to its workspace + IAM-style policy. Downstream services use this with a 60s LRU cache and evaluate the policy via @repo/authz-nestjs evaluatePolicy().","tags":["introspect"]}},"/v1/workspaces":{"post":{"operationId":"WorkspaceController_createWorkspace","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponseDto"}}}}},"summary":"Create a new workspace","tags":["workspaces"]},"get":{"operationId":"WorkspaceController_listWorkspaces","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspacesResponseDto"}}}}},"summary":"List workspaces the current user belongs to","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}":{"get":{"operationId":"WorkspaceController_getWorkspace","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponseDto"}}}}},"summary":"Get workspace details","tags":["workspaces"]},"patch":{"operationId":"WorkspaceController_updateWorkspace","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceDto"}}}},"responses":{"200":{"description":"Workspace updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponseDto"}}}},"400":{"description":"Invalid slug or display name."},"404":{"description":"Workspace not found."},"409":{"description":"Slug already taken by another workspace."}},"summary":"Update workspace display name, slug, or settings. Slug renames broadcast a `workspace.renamed` event so denormalised consumers (rally-api) sync.","tags":["workspaces"]},"delete":{"operationId":"WorkspaceController_deleteWorkspace","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Workspace deleted."}},"summary":"Delete a workspace (requires workspace.delete)","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}/members":{"get":{"operationId":"WorkspaceController_listMembers","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspaceMembersResponseDto"}}}}},"summary":"List workspace members","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}/members/{account_id}/role":{"patch":{"operationId":"WorkspaceController_updateMemberRole","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"account_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberRoleDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberRoleResponseDto"}}}}},"summary":"Update a member's role. Body: { roleId } or { role: \"owner\"|\"admin\"|\"member\" }.","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}/members/{account_id}":{"delete":{"operationId":"WorkspaceController_removeMember","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"account_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Member removed."}},"summary":"Remove a member from a workspace (or self-leave)","tags":["workspaces"]}},"/v1/workspaces/invites/accept":{"post":{"operationId":"WorkspaceController_acceptInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptWorkspaceInviteDto"}}}},"responses":{"201":{"description":"The joined workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResponseDto"}}}}},"summary":"Accept a workspace invite by code","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}/invites":{"post":{"operationId":"WorkspaceController_createInvite","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceInviteDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceInviteResponseDto"}}}}},"summary":"Create a workspace invite link","tags":["workspaces"]},"get":{"operationId":"WorkspaceController_listInvites","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspaceInvitesResponseDto"}}}}},"summary":"List workspace invites","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}/invites/{invite_id}":{"delete":{"operationId":"WorkspaceController_revokeInvite","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"invite_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Invite revoked."}},"summary":"Revoke a workspace invite","tags":["workspaces"]}},"/v1/workspaces/{workspace_slug}/roles":{"get":{"operationId":"WorkspaceRoleController_list","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspaceRolesResponseDto"}}}}},"summary":"List all roles in a workspace","tags":["workspace-roles"]},"post":{"operationId":"WorkspaceRoleController_create","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRoleDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponseDto"}}}}},"summary":"Create a new custom role","tags":["workspace-roles"]}},"/v1/workspaces/{workspace_slug}/roles/{role_id}":{"get":{"operationId":"WorkspaceRoleController_get","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"role_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponseDto"}}}}},"summary":"Get a role + its permissions","tags":["workspace-roles"]},"patch":{"operationId":"WorkspaceRoleController_update","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"role_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRoleDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRoleResponseDto"}}}}},"summary":"Update a role or replace its permission set","tags":["workspace-roles"]},"delete":{"operationId":"WorkspaceRoleController_delete","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"role_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Custom role deleted."}},"summary":"Delete a custom role (fails if it has members)","tags":["workspace-roles"]}},"/v1/workspaces/{workspace_slug}/policies":{"get":{"operationId":"WorkspacePolicyController_list","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListManagedPoliciesResponseDto"}}}}},"summary":"List every managed policy in a workspace","tags":["workspace-policies"]},"post":{"operationId":"WorkspacePolicyController_create","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspacePolicyDto"}}}},"responses":{"201":{"description":"Policy created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedPolicyResponseDto"}}}},"409":{"description":"Name already in use."}},"summary":"Create a managed policy","tags":["workspace-policies"]}},"/v1/workspaces/{workspace_slug}/policies/actions/catalog":{"get":{"operationId":"WorkspacePolicyController_catalog","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceActionCatalogResponseDto"}}}}},"summary":"List every known workspace action (catalog of statement actions).","tags":["workspace-policies"]}},"/v1/workspaces/{workspace_slug}/policies/{policy_id}":{"get":{"operationId":"WorkspacePolicyController_get","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"policy_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Policy detail. `binding_count.{pak,role}` reports how many things are currently bound — surface this in delete-confirm UX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedPolicyDetailResponseDto"}}}},"404":{"description":"Policy not found in this workspace."}},"summary":"Get a managed policy by id","tags":["workspace-policies"]},"patch":{"operationId":"WorkspacePolicyController_update","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"policy_id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspacePolicyDto"}}}},"responses":{"200":{"description":"Updated policy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedPolicyDetailResponseDto"}}}},"400":{"description":"Invalid policy shape, name, or condition block."},"403":{"description":"Caller cannot grant one or more requested actions."},"404":{"description":"Policy not found in this workspace."},"409":{"description":"Name already in use."}},"summary":"Update a managed policy","tags":["workspace-policies"]},"delete":{"operationId":"WorkspacePolicyController_delete","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"policy_id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Policy deleted."},"404":{"description":"Policy not found in this workspace."}},"summary":"Delete a managed policy. Cascades: API keys lose the binding (deny-all if no other bindings remain); roles fall back to their inline policy column.","tags":["workspace-policies"]}},"/v1/workspaces/{workspace_slug}/services":{"get":{"operationId":"WorkspaceServiceController_list","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspaceServicesResponseDto"}}}}},"summary":"List services activated on this workspace","tags":["workspace-services"]}},"/v1/workspaces/{workspace_slug}/services/{service}":{"post":{"operationId":"WorkspaceServiceController_enable","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"service","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnableWorkspaceServiceDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceServiceStateDto"}}}}},"summary":"Enable (or re-enable) a service for this workspace","tags":["workspace-services"]},"delete":{"operationId":"WorkspaceServiceController_disable","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"service","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Service disabled. Settings preserved for re-enable."}},"summary":"Disable a service (settings are retained)","tags":["workspace-services"]}},"/v1/workspaces/{workspace_slug}/services/{service}/settings":{"patch":{"operationId":"WorkspaceServiceController_updateSettings","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"service","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Replaces the per-service settings jsonb entirely. The body is the settings object itself (no wrapper). Service-specific shape — e.g. envoi tracks default sender, agora tracks ranking weights.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"example":{"default_sender":"noreply@acme.com"}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceServiceStateDto"}}}}},"summary":"Replace the settings jsonb for an active service","tags":["workspace-services"]}},"/v1/workspaces/{workspace_slug}/api-keys":{"get":{"operationId":"PlatformApiKeyController_list","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicApiKeyDto"}}}}}},"security":[{"bearer":[]}],"summary":"List PAKs in this workspace. Plaintext tokens are NOT returned — only prefixes + metadata.","tags":["platform-api-keys"]},"post":{"operationId":"PlatformApiKeyController_create","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePakDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintPakResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Mint a new PAK. The plaintext `token` is returned ONCE — store it now. The minter can grant at most their own effective permissions.","tags":["platform-api-keys"]}},"/v1/workspaces/{workspace_slug}/api-keys/{id}/bindings":{"patch":{"operationId":"PlatformApiKeyController_setBindings","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetBindingsDto"}}}},"responses":{"200":{"description":"Updated PAK with new bindings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiKeyDto"}}}},"400":{"description":"One of: bound > 25 policies; merged > 200 statements; one of the policy ids does not exist or lives in a different workspace."},"403":{"description":"Caller cannot grant one or more actions in the merged statement set."},"404":{"description":"PAK not found in this workspace."},"409":{"description":"Empty bindings — revoke the PAK instead of leaving it unbound."}},"security":[{"bearer":[]}],"summary":"Replace the managed-policy bindings on a PAK. Re-runs caller-narrowing on the merged union of statements.","tags":["platform-api-keys"]}},"/v1/workspaces/{workspace_slug}/api-keys/{id}":{"get":{"operationId":"PlatformApiKeyController_get","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiKeyDto"}}}}},"security":[{"bearer":[]}],"summary":"Get a single PAK by id.","tags":["platform-api-keys"]},"patch":{"operationId":"PlatformApiKeyController_update","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePakDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicApiKeyDto"}}}}},"security":[{"bearer":[]}],"summary":"Update a PAK’s name or description (does not touch bindings).","tags":["platform-api-keys"]},"delete":{"operationId":"PlatformApiKeyController_revoke","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"force","required":false,"in":"query","description":"When `true`, publish a `pak:revoked` Redis fan-out so subscribed services evict their caches immediately instead of waiting out the 60s TTL.","schema":{"type":"boolean"}}],"responses":{"204":{"description":"PAK revoked."}},"security":[{"bearer":[]}],"summary":"Revoke a PAK. Default behaviour: introspect returns null on the next upstream call; downstream services fail-closed within the 60s cache TTL window. Pass `?force=true` to additionally publish a Redis fan-out that evicts the matching cache entry across every PAK-consuming service immediately — use it when rotating a leaked PAK and the 60s window matters.","tags":["platform-api-keys"]}},"/v1/workspaces/{workspace_slug}/audit-logs":{"get":{"operationId":"WorkspaceAuditController_listAuditLogs","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWorkspaceAuditLogsResponseDto"}}}}},"summary":"List workspace audit log entries","tags":["workspace-audit-logs"]}},"/v1/workspaces/{workspace_slug}/audit-feed":{"get":{"description":"Merged audit timeline across the workspace surfaces. v1 covers `workspace_audit` (this service); per-app Heimdall and per-community Agora rows arrive in a follow-up. Response shape is stable from v1 — clients can build against it today.","operationId":"WorkspaceAuditFeedController_listFeed","parameters":[{"name":"workspace_slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"since","required":false,"in":"query","description":"ISO-8601 lower bound on `timestamp`. Items older than `since` are not returned.","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from a previous response's `pagination.next_cursor`. Resumes the feed at the same merge boundary.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Per-page cap. Clamped to 1..500; default 100.","schema":{"type":"number"}},{"name":"sources","required":false,"in":"query","description":"Comma-separated subset of sources to include (e.g. `workspace_audit,heimdall_audit`). Omitted = all enabled sources.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditFeedResponseDto"}}}},"400":{"description":"Invalid query params — `since` not parseable as ISO-8601, or `sources` references an unknown source name. Unrecognized sources are silently dropped; an empty result set after that filter still returns 200."},"403":{"description":"The caller is signed in but lacks `workspace.audit.read` for this workspace."},"404":{"description":"The `:workspaceSlug` path param does not resolve to a workspace the caller can see."}},"summary":"Unified workspace audit feed (cursor-paginated)","tags":["workspace-audit-logs"]}},"/v1/account":{"delete":{"operationId":"AccountController_deleteAccount","parameters":[],"responses":{"204":{"description":"Account deleted. All sessions revoked."},"403":{"description":"The session lacks the required action for self-account deletion (defence-in-depth — the route is guarded but the cookie may be present without action access)."},"404":{"description":"No account row exists for the session principal. Should never fire for a valid session — surfaced for SDKs."}},"summary":"Delete the calling account. Auto-promotes the oldest admin (or oldest non-self member) to owner in any workspace where the departing account is the sole owner.","tags":["account"]}},"/v1/session":{"get":{"operationId":"SessionController_getSessions","parameters":[],"responses":{"200":{"description":"Active sessions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionDto"}}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"List active sessions","tags":["session"]}},"/v1/verification/verify":{"post":{"operationId":"VerificationController_verify","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCodeDto"}}}},"responses":{"200":{"description":"Email verified successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackResponseDto"}}}},"400":{"description":"Invalid or expired verification code"}},"summary":"Verify an email code","tags":["verification"]}},"/v1/verification/resend":{"post":{"operationId":"VerificationController_resend","parameters":[],"responses":{"200":{"description":"Verification email sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackResponseDto"}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Resend verification email","tags":["verification"]}},"/.well-known/jwks.json":{"get":{"operationId":"JwksController_jwks","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JwksResponseDto"}}}}},"summary":"Public JWKS for verifying platform-issued JWTs (issuer `https://api.auth.productcraft.co`).","tags":["jwks"]}}},"info":{"title":"Platform Auth API","description":"PlatformUser authentication and workspace administration for ProductCraft. Owns sign-in, sign-up, password reset, workspaces, members, invites, workspace roles, workspace-service toggles, and the introspect endpoint every other backend uses to resolve permissions.","version":"1.0.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"SignUpDto":{"type":"object","properties":{"email":{"type":"string","description":"User email address","example":"user@example.com"},"username":{"type":"string","description":"Unique username (letters, numbers, dot, underscore, hyphen)","example":"john_doe"},"password":{"type":"string","description":"User password (min 8 characters)","example":"MySecurePassword123"},"display_name":{"type":"string","description":"Display name","example":"John Doe"},"session_duration":{"type":"object","description":"Session duration: \"short\" (24h), \"long\" (90d), or integer seconds (3600–7776000). Defaults to 30 days.","example":"long"}},"required":["email","username","password"]},"TokenResponseDto":{"type":"object","properties":{"access_token":{"type":"string","description":"Access token (JWT)","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"refresh_token":{"type":"string","description":"Refresh token (JWT)","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"token_type":{"type":"string","description":"Token type","example":"Bearer"},"expires_in":{"type":"number","description":"Access token TTL in seconds","example":3600}},"required":["access_token","token_type","expires_in"]},"SignInDto":{"type":"object","properties":{"identifier":{"type":"string","description":"Email or username","example":"user@example.com"},"password":{"type":"string","description":"User password","example":"MySecurePassword123"},"session_duration":{"type":"object","description":"Session duration: \"short\" (24h), \"long\" (90d), or integer seconds (3600–7776000). Defaults to 30 days.","example":"short"}},"required":["identifier","password"]},"RefreshDto":{"type":"object","properties":{"refresh_token":{"type":"string","description":"Refresh token (JWT)","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}},"required":["refresh_token"]},"LogoutDto":{"type":"object","properties":{"refresh_token":{"type":"string","description":"Refresh token (JWT)","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}},"required":["refresh_token"]},"RequestPasswordResetDto":{"type":"object","properties":{"email":{"type":"string","description":"Email address to send reset token","example":"user@example.com"}},"required":["email"]},"ResetPasswordDto":{"type":"object","properties":{"token":{"type":"string","description":"6-digit reset token sent via email","example":"123456"},"new_password":{"type":"string","description":"New password (min 8 characters)","example":"MyNewPassword123"}},"required":["token","new_password"]},"IntrospectPrincipalDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sub":{"type":"string","format":"uuid"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}},"required":["id","sub","email","name"]},"IntrospectAccountDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","nullable":true},"username":{"type":"string"},"display_name":{"type":"string","nullable":true},"status":{"type":"string","description":"Account status (e.g. `active`, `suspended`)."},"email_verified_at":{"type":"string","nullable":true,"format":"date-time","description":"Timestamp when this account verified its primary email, or null if unverified."}},"required":["id","email","username","display_name","status","email_verified_at"]},"IntrospectWorkspaceListItemDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"display_name":{"type":"string"},"role":{"type":"string","description":"Caller’s role name in this workspace.","example":"owner"},"services":{"description":"Enabled services on this workspace.","example":["envoi","rally"],"type":"array","items":{"type":"string"}}},"required":["id","slug","display_name","role","services"]},"IntrospectResponseDto":{"type":"object","properties":{"is_authenticated":{"type":"boolean"},"principal":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/IntrospectPrincipalDto"}]},"account":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/IntrospectAccountDto"}]},"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/IntrospectWorkspaceListItemDto"}}},"required":["is_authenticated"]},"IntrospectRoleDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"PolicyStatementDto":{"type":"object","properties":{"effect":{"type":"string","description":"Whether the statement allows or denies the listed actions on the listed resources.","enum":["allow","deny"],"example":"allow"},"actions":{"description":"Action strings the statement applies to. Wildcards (e.g. `agora.*`, `*.read`, `*`) supported per `actionMatches` semantics.","example":["agora.read","agora.list"],"minItems":1,"type":"array","items":{"type":"string"}},"resources":{"description":"Resource URNs the statement applies to. Use `[\"*\"]` for workspace-wide. Per-resource scoping (e.g. `pcft:agora:community/<uuid>`) is supported by the PAK lane today; the workspace-role authoring API restricts this to `[\"*\"]` for now (see `tasks/platform-auth-api/001`).","example":["*"],"minItems":1,"type":"array","items":{"type":"string"}}},"required":["effect","actions","resources"]},"IntrospectWorkspaceResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Canonical workspace UUID. Only present for members — non-members get a uniform low-information response.","format":"uuid"},"is_member":{"type":"boolean"},"role":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/IntrospectRoleDto"}]},"policy":{"description":"Effective IAM-style policy for this caller in this workspace.","type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"services":{"description":"Enabled services on this workspace.","example":["envoi","rally"],"type":"array","items":{"type":"string"}}},"required":["is_member","role","policy","services"]},"IntrospectApiKeyResponseDto":{"type":"object","properties":{"is_valid":{"type":"boolean"},"workspace_id":{"type":"string","format":"uuid"},"policy":{"description":"Merged effective policy across every managed policy bound to this PAK.","type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"created_by":{"type":"string","nullable":true,"format":"uuid"},"api_key_id":{"type":"string","format":"uuid"}},"required":["is_valid","workspace_id","policy","created_by","api_key_id"]},"CreateWorkspaceDto":{"type":"object","properties":{"slug":{"type":"string","description":"URL-friendly slug. Lowercase alphanumerics, separated by hyphens. Used in URLs and as a stable handle.","example":"acme-corp","minLength":2,"maxLength":64},"display_name":{"type":"string","description":"Display name shown in console and on receipts.","example":"Acme Corp","minLength":1,"maxLength":128}},"required":["slug","display_name"]},"WorkspaceResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string","example":"acme-corp"},"display_name":{"type":"string","example":"Acme Corp"},"created_by":{"type":"string","format":"uuid"},"status":{"type":"string","description":"Workspace status (e.g. `active`).","example":"active"},"settings":{"type":"object","description":"Free-form workspace settings blob."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","slug","display_name","created_by","status","settings","created_at","updated_at"]},"WorkspaceListItemDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string","example":"acme-corp"},"display_name":{"type":"string","example":"Acme Corp"},"created_by":{"type":"string","format":"uuid"},"status":{"type":"string","description":"Workspace status (e.g. `active`).","example":"active"},"settings":{"type":"object","description":"Free-form workspace settings blob."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"member_role_id":{"type":"string","nullable":true,"format":"uuid","description":"Caller’s workspace_role.id for this workspace."},"member_role":{"type":"string","nullable":true,"description":"Caller’s role name in this workspace.","example":"owner"}},"required":["id","slug","display_name","created_by","status","settings","created_at","updated_at","member_role_id","member_role"]},"PaginationDto":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true,"example":null},"has_more":{"type":"boolean","example":false}},"required":["next_cursor","has_more"]},"ListWorkspacesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceListItemDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"}},"required":["data","pagination"]},"UpdateWorkspaceDto":{"type":"object","properties":{"display_name":{"type":"string","minLength":1,"maxLength":128},"slug":{"type":"string","description":"New URL-friendly slug. Same constraints as the create form. Renames are broadcast on the workspace events queue so denormalised consumers (rally-api, future product surfaces) can sync. Old URLs continue to resolve for one rename cycle.","minLength":2,"maxLength":64,"example":"acme"},"settings":{"type":"object","description":"Free-form workspace settings blob."}}},"WorkspaceMemberResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"username":{"type":"string"},"display_name":{"type":"string","nullable":true},"primary_email":{"type":"string","nullable":true},"role_id":{"type":"string","format":"uuid"},"role":{"type":"string"},"role_is_system":{"type":"boolean"},"joined_at":{"type":"string","format":"date-time"}},"required":["id","account_id","username","display_name","primary_email","role_id","role","role_is_system","joined_at"]},"ListWorkspaceMembersResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMemberResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"}},"required":["data","pagination"]},"UpdateMemberRoleDto":{"type":"object","properties":{"role_id":{"type":"string","description":"UUID of the role to assign. Use this to target custom roles.","example":"11111111-1111-4111-8111-111111111111"},"role":{"type":"string","description":"System role shortcut: 'owner' | 'admin' | 'member'. Either `roleId` or `role` must be supplied.","enum":["owner","admin","member"]}}},"UpdateMemberRoleResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"role_id":{"type":"string","format":"uuid"},"invited_by":{"type":"string","nullable":true,"format":"uuid"},"joined_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","account_id","role_id","invited_by","joined_at"]},"AcceptWorkspaceInviteDto":{"type":"object","properties":{"code":{"type":"string","description":"Invite code from the invite email / link.","example":"inv_…"}},"required":["code"]},"CreateWorkspaceInviteDto":{"type":"object","properties":{"email":{"type":"string","description":"Optional invitee email. When set, the invite is locked to that address.","example":"alice@example.com"},"role_id":{"type":"string","description":"UUID of the role to grant on accept. Use to target custom roles."},"role":{"type":"string","description":"System role shortcut: 'owner' | 'admin' | 'member'.","enum":["owner","admin","member"]},"max_uses":{"type":"number","description":"Maximum number of accepts. Defaults to 1. Use higher values for \"team link\" invites.","example":1,"minimum":1,"maximum":10000},"expires_in_hours":{"type":"number","description":"Hours until the invite expires. Defaults to 168 (7 days).","example":168,"minimum":1,"maximum":8760}}},"WorkspaceInviteResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"code":{"type":"string","description":"Single-use invite code embedded in the invite URL."},"email":{"type":"string","nullable":true,"format":"email"},"role_id":{"type":"string","nullable":true,"format":"uuid"},"max_uses":{"type":"number","nullable":true},"use_count":{"type":"number"},"expires_at":{"type":"string","nullable":true,"format":"date-time"},"revoked_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid"}},"required":["id","workspace_id","code","email","role_id","max_uses","use_count","expires_at","revoked_at","created_at","created_by"]},"WorkspaceInviteListItemDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"code":{"type":"string","description":"Single-use invite code embedded in the invite URL."},"email":{"type":"string","nullable":true,"format":"email"},"role_id":{"type":"string","nullable":true,"format":"uuid"},"max_uses":{"type":"number","nullable":true},"use_count":{"type":"number"},"expires_at":{"type":"string","nullable":true,"format":"date-time"},"revoked_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid"},"role_name":{"type":"string","nullable":true}},"required":["id","workspace_id","code","email","role_id","max_uses","use_count","expires_at","revoked_at","created_at","created_by","role_name"]},"ListWorkspaceInvitesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceInviteListItemDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"}},"required":["data","pagination"]},"WorkspaceRoleResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Role name. System role names are `owner`/`admin`/`member`."},"description":{"type":"string","nullable":true},"is_system":{"type":"boolean","description":"True for built-in system roles (owner/admin/member); false for custom roles."},"policy":{"description":"Effective IAM-style policy. When `policy_id` is set this is the bound managed policy; otherwise it is the inline policy column.","type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"policy_id":{"type":"string","nullable":true,"format":"uuid","description":"Managed policy ID if the role is bound to one, else null."}},"required":["id","name","description","is_system","policy","policy_id"]},"ListWorkspaceRolesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceRoleResponseDto"}}},"required":["data"]},"CreateWorkspaceRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Role name. Free-form; not URL-significant.","example":"BillingAdmin","minLength":1,"maxLength":64},"description":{"type":"string","description":"Optional human-readable role description.","maxLength":256},"policy":{"description":"IAM-style policy granting the role its permissions. Same wire shape as PAK policies. Omit or pass `[]` for a no-permissions role.","example":[{"effect":"allow","actions":["envoi.*.read"],"resources":["*"]}],"type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"policy_id":{"type":"string","description":"Optional managed-policy id to bind the role to. When set, the role's effective policy comes from the bound managed policy and `policy` (inline) must be omitted.","nullable":true,"example":"11111111-1111-1111-1111-111111111111"}},"required":["name"]},"UpdateWorkspaceRoleDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":256},"policy":{"description":"Replaces the role policy if provided. Omit to leave policy unchanged.","type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"policy_id":{"type":"string","description":"Bind / unbind the role to a managed policy. UUID → bind, null → unbind, omit → leave unchanged.","nullable":true}}},"ManagedPolicyResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"envoi-readonly"},"description":{"type":"string","nullable":true},"policy":{"description":"IAM-style policy statements bound to this managed policy.","type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"created_by":{"type":"string","nullable":true,"format":"uuid","description":"Account that authored the row. Only revealed to callers with `workspace.audit.read`; plain members see null."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","name","description","policy","created_by","created_at","updated_at"]},"ListManagedPoliciesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ManagedPolicyResponseDto"}}},"required":["data"]},"WorkspaceActionCatalogResponseDto":{"type":"object","properties":{"actions":{"description":"Every known workspace action across all enabled service catalogs.","type":"array","items":{"type":"string"}}},"required":["actions"]},"PolicyBindingCountDto":{"type":"object","properties":{"pak":{"type":"number","description":"Number of platform API keys bound to this policy."},"role":{"type":"number","description":"Number of workspace roles bound to this policy."}},"required":["pak","role"]},"ManagedPolicyDetailResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"envoi-readonly"},"description":{"type":"string","nullable":true},"policy":{"description":"IAM-style policy statements bound to this managed policy.","type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}},"created_by":{"type":"string","nullable":true,"format":"uuid","description":"Account that authored the row. Only revealed to callers with `workspace.audit.read`; plain members see null."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"binding_count":{"description":"Counts of currently-bound consumers — surface this in delete-confirm UX so the operator sees cascade impact.","allOf":[{"$ref":"#/components/schemas/PolicyBindingCountDto"}]}},"required":["id","workspace_id","name","description","policy","created_by","created_at","updated_at","binding_count"]},"CreateWorkspacePolicyDto":{"type":"object","properties":{"name":{"type":"string","description":"Policy name (1-64 chars, alphanumeric + space/underscore/hyphen). Unique per workspace.","example":"envoi-readonly","minLength":1,"maxLength":64},"description":{"type":"string","description":"Optional human description. Pass `null` to clear an existing description on PATCH.","nullable":true,"example":"Read-only access to all Envoi resources"},"policy":{"description":"IAM-style policy. Empty array means \"no permissions\" — useful as a placeholder before granting any actions.","example":[{"effect":"allow","actions":["envoi.read","envoi.list"],"resources":["*"]}],"type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}}},"required":["name","policy"]},"UpdateWorkspacePolicyDto":{"type":"object","properties":{"name":{"type":"string","description":"Policy name (1-64 chars, alphanumeric + space/underscore/hyphen). Unique per workspace.","example":"envoi-readonly","minLength":1,"maxLength":64},"description":{"type":"string","description":"Optional human description. Pass `null` to clear an existing description on PATCH.","nullable":true,"example":"Read-only access to all Envoi resources"},"policy":{"description":"IAM-style policy. Empty array means \"no permissions\" — useful as a placeholder before granting any actions.","example":[{"effect":"allow","actions":["envoi.read","envoi.list"],"resources":["*"]}],"type":"array","items":{"$ref":"#/components/schemas/PolicyStatementDto"}}}},"WorkspaceServiceStateDto":{"type":"object","properties":{"workspace_id":{"type":"string","format":"uuid"},"service":{"type":"string","description":"Service identifier (e.g. `envoi`, `rally`, `agora`, `heimdall`).","example":"envoi"},"enabled":{"type":"boolean"},"enabled_at":{"type":"string","nullable":true,"format":"date-time"},"enabled_by":{"type":"string","nullable":true,"format":"uuid"},"disabled_at":{"type":"string","nullable":true,"format":"date-time"},"disabled_by":{"type":"string","nullable":true,"format":"uuid"},"settings":{"type":"object","description":"Per-service settings blob (service-specific shape)."}},"required":["workspace_id","service","enabled","enabled_at","enabled_by","disabled_at","disabled_by","settings"]},"ListWorkspaceServicesResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceServiceStateDto"}}},"required":["data"]},"EnableWorkspaceServiceDto":{"type":"object","properties":{"settings":{"type":"object","description":"Optional per-service settings blob captured at enable time."}}},"BoundPolicySummaryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"envoi-readonly"},"description":{"type":"string","nullable":true}},"required":["id","name","description"]},"PublicApiKeyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"created_by":{"type":"string","nullable":true,"format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"token_prefix":{"type":"string","description":"The first ~14 chars of the token (prefix). Plaintext token is never returned after mint.","example":"pcft_live_abc123"},"policies":{"description":"Managed policies currently bound to this PAK.","type":"array","items":{"$ref":"#/components/schemas/BoundPolicySummaryDto"}},"last_used_at":{"type":"string","nullable":true,"format":"date-time"},"revoked_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","created_by","name","description","token_prefix","policies","last_used_at","revoked_at","created_at","updated_at"]},"CreatePakDto":{"type":"object","properties":{"name":{"type":"string","description":"Human-readable PAK name (shown in the keys list).","example":"Production agora client","minLength":1,"maxLength":120},"description":{"type":"string","description":"Optional description (e.g. which service uses the key).","maxLength":500},"policy_ids":{"description":"IDs of managed policies (workspace_policy rows) to bind to this PAK. Bind one or more — the PAK's effective policy is the union of all bound statements.","example":["11111111-1111-1111-1111-111111111111"],"type":"array","items":{"type":"string"}}},"required":["name","policy_ids"]},"MintPakResponseDto":{"type":"object","properties":{"token":{"type":"string","description":"The full plaintext PAK token. Returned ONCE — store it now; it cannot be retrieved later.","example":"pcft_live_abc123XYZ..."},"record":{"$ref":"#/components/schemas/PublicApiKeyDto"}},"required":["token","record"]},"SetBindingsDto":{"type":"object","properties":{"policy_ids":{"description":"Replacement set of managed-policy IDs. Empty array is rejected — revoke the PAK to disable it.","type":"array","items":{"type":"string"}}},"required":["policy_ids"]},"UpdatePakDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","nullable":true,"maxLength":500}}},"WorkspaceAuditLogEntryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"actor_id":{"type":"string","nullable":true,"description":"Account UUID of the actor, or null for system actions."},"actor_type":{"type":"string","description":"Actor classification.","enum":["platform_user","system","pat"],"example":"platform_user"},"action":{"type":"string","description":"Action identifier (e.g. `workspace.member.removed`).","example":"workspace.member.removed"},"resource":{"type":"string","description":"Resource type the action targeted.","example":"workspace_membership"},"resource_id":{"type":"string","nullable":true,"format":"uuid"},"ip":{"type":"string","nullable":true,"description":"Originating request IP, when available."},"metadata":{"type":"object","description":"Free-form metadata attached to the audit row (jsonb)."},"created_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","actor_id","actor_type","action","resource","resource_id","ip","metadata","created_at"]},"ListWorkspaceAuditLogsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceAuditLogEntryDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"}},"required":["data","pagination"]},"FeedItemDto":{"type":"object","properties":{"source":{"type":"string","description":"Which audit table this row came from.","enum":["workspace_audit","heimdall_audit","agora_moderation"],"example":"workspace_audit"},"timestamp":{"type":"string","format":"date-time"},"actor_id":{"type":"string","nullable":true,"format":"uuid"},"actor_type":{"type":"string","description":"Actor classification — one of 'platform_user' | 'end_user' | 'm2m' | 'system' | 'pat' | 'api_key'.","example":"platform_user"},"action":{"type":"string","example":"workspace.member.removed"},"resource":{"type":"string","example":"workspace_membership"},"resource_id":{"type":"string","nullable":true,"format":"uuid"},"metadata":{"type":"object","description":"Free-form metadata (jsonb)."},"source_ref":{"type":"string","description":"Stable per-source identifier. Used by the cursor to resume from the same point on the next page.","example":"4e5f5c9e-b4ee-4401-9275-283feb66c178"}},"required":["source","timestamp","actor_id","actor_type","action","resource","resource_id","metadata","source_ref"]},"SourceDiagnosticDto":{"type":"object","properties":{"source":{"type":"string","enum":["workspace_audit","heimdall_audit","agora_moderation"]},"included":{"type":"boolean"},"count":{"type":"number"},"error":{"type":"string"}},"required":["source","included","count"]},"AuditFeedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FeedItemDto"}},"pagination":{"$ref":"#/components/schemas/PaginationDto"},"sources":{"description":"Per-source diagnostics so the UI / customer can see which sources contributed to this page and which are stubbed pending the cross-service fanout.","type":"array","items":{"$ref":"#/components/schemas/SourceDiagnosticDto"}}},"required":["data","pagination","sources"]},"SessionDto":{"type":"object","properties":{"id":{"type":"string","description":"Session identifier","example":"sess_01HZY2G3A7M8Q9W4E6R2K1"},"account_id":{"type":"string","description":"Owning account identifier","example":"acc_01HZY2F2B6N7M8Q9W4E6R2"},"ip":{"type":"string","description":"Client IP address","nullable":true,"example":"203.0.113.42"},"user_agent":{"type":"string","description":"HTTP User-Agent string","nullable":true,"example":"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"},"created_at":{"type":"string","description":"Creation timestamp (ISO 8601)","format":"date-time","example":"2025-09-20T12:34:56.000Z"},"expires_at":{"type":"string","description":"Expiration timestamp (ISO 8601)","format":"date-time","example":"2025-09-21T12:34:56.000Z"},"last_used_at":{"type":"string","description":"Last time this session was used (ISO 8601) or null","nullable":true,"format":"date-time","example":"2025-09-20T18:00:00.000Z"}},"required":["id","account_id","ip","user_agent","created_at","expires_at","last_used_at"]},"VerifyCodeDto":{"type":"object","properties":{"code":{"type":"string","description":"Verification code from the email link.","example":"a1b2c3d4…"}},"required":["code"]},"FeedbackResponseDto":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable message","example":"Email verified successfully"}},"required":["message"]},"JwkDto":{"type":"object","properties":{"kty":{"type":"string","description":"Key type (e.g. `RSA`).","example":"RSA"},"alg":{"type":"string","description":"Signing algorithm (e.g. `RS256`).","example":"RS256"},"kid":{"type":"string","description":"Key ID for matching the kid header on a JWT.","example":"k_2026_05_01"},"use":{"type":"string","description":"Key usage. Always `sig` for this endpoint.","example":"sig"}},"required":["kty","alg","kid","use"]},"JwksResponseDto":{"type":"object","properties":{"keys":{"description":"Array of public JWKs. Token verifiers iterate these by `kid` to find the matching signer.","type":"array","items":{"$ref":"#/components/schemas/JwkDto"}}},"required":["keys"]}}}}