{"openapi":"3.0.0","paths":{"/":{"get":{"description":"Unauthenticated HTML: what this service is, links to the API reference and the OpenAPI document, and the current build state. This is the one route that is NOT wrapped in the standard response envelope.","operationId":"AppController_landing","parameters":[],"responses":{"200":{"description":"The landing page.","content":{"text/html":{"schema":{"type":"string","format":"html"}}}}},"summary":"Landing page","tags":["Health"]}},"/workspaces":{"get":{"description":"Reads exactly the workspace ids the principal already carries, so there is no other workspace to leak into - the one route here that legitimately names no target workspace.","operationId":"TenancyController_workspaces","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceResponseDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List the caller's workspaces","tags":["Tenancy"]}},"/groups":{"get":{"description":"Every group in the workspace, each with its entities, each of those with its branches. One call returns the whole hierarchy in the shape the structure screen renders, rather than making the client assemble it from separate lists.","operationId":"TenancyController_groups","parameters":[{"name":"workspaceId","required":true,"in":"query","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/GroupWithEntitiesDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Read the corporate structure","tags":["Tenancy"]},"post":{"description":"A holding company: the top of an enterprise group hierarchy.","operationId":"TenancyController_group","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroupDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/GroupResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Create a group","tags":["Tenancy"]}},"/groups/{groupId}/entities":{"post":{"description":"A subsidiary or legal entity. The group must be in the caller's own tenant: the link is a composite foreign key on (groupId, workspaceId), so the database itself rejects an entity hung off another tenant's group.","operationId":"TenancyController_entity","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEntityDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/EntityResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Add an entity to a group","tags":["Tenancy"]}},"/entities/{entityId}/branches":{"post":{"description":"A branch, campus, region or unit - the third level of the group hierarchy. Like entities, the parent is looked up through the caller's own scope first: a foreign-key check alone would validate against an entity the caller cannot see, because Postgres runs referential-integrity triggers with row security off.","operationId":"TenancyController_branch","parameters":[{"name":"entityId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBranchDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/BranchResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Add a branch to an entity","tags":["Tenancy"]}},"/auth/register":{"post":{"description":"Creates the user, mints them a fresh ORGANIZATION workspace, makes them its Org Admin, and queues a verification email. No session is minted and none can be: `POST /auth/login` refuses the account with 403 `email_unverified` until the link in that mail is opened. Rate limited to 30/min per IP.","operationId":"AuthController_register","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/RegisterResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"That email is already registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"429":{"description":"Rate limit exceeded (30/min).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Register an account","tags":["Auth"]}},"/auth/login":{"post":{"description":"On success with MFA off, sets the `sid` session cookie and returns `{ status: \"ok\" }`. With MFA on, sets NO cookie and returns `{ status: \"mfa_required\", challengeId }` to exchange at `/auth/mfa/verify`. A completed login revokes every other session the account held: one live session per account, so signing in on a new device signs the old one out. An account whose address has never been verified is refused with 403 `email_unverified` - after, never before, the password check, so the refusal is not an enumeration oracle. Repeated failures lock the account; the response never distinguishes a wrong password from an unknown email. Rate limited to 50/min per IP.","operationId":"AuthController_login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/LoginResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Invalid credentials, or the account is locked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"The address has not been verified (`email_unverified`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"429":{"description":"Rate limit exceeded (50/min).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Log in","tags":["Auth"]}},"/auth/me":{"get":{"description":"The caller's user id, readable workspaces, and roles.","operationId":"AuthController_me","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/MeResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"The current principal","tags":["Auth"]}},"/auth/logout":{"post":{"description":"Revokes the caller's own session and clears the cookie. Reachable even when the MFA gate would otherwise block the session.","operationId":"AuthController_logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/OkResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Log out","tags":["Auth"]}},"/auth/sessions/revoke-all":{"post":{"description":"Including the one making this call.","operationId":"AuthController_revokeAllSessions","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/OkResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Revoke every session","tags":["Auth"]}},"/auth/mfa/enroll":{"post":{"description":"Returns a TOTP secret and its otpauth URL. Body is optional on a first enrolment; re-enrolling an account that already has MFA on requires a step-up credential.","operationId":"AuthController_enroll","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollMfaDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/EnrollMfaResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Begin MFA enrolment","tags":["Auth"]}},"/auth/mfa/activate":{"post":{"description":"Confirms enrolment with a live TOTP code and returns single-use recovery codes, shown once. Rotates the caller onto a fresh MFA-satisfied session and revokes every other session they held, so a stolen pre-activation session cannot outlive activation.","operationId":"AuthController_activate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivateMfaDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/ActivateMfaResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"The token did not verify.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Activate MFA","tags":["Auth"]}},"/auth/mfa/verify":{"post":{"description":"Exchanges the `challengeId` from a `mfa_required` login plus a TOTP or recovery code for a session cookie. The challenge is single-use. Like the password-only path, the session it mints revokes every other session the account held.","operationId":"AuthController_verifyMfa","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyMfaDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/LoginResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"Unknown or spent challenge, or a bad token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Complete an MFA login","tags":["Auth"]}},"/auth/password/request":{"post":{"description":"Always returns `{ ok: true }`, whether or not the email is registered - the response must never be the signal used to enumerate accounts. Rate limited to 20/min per IP.","operationId":"AuthController_requestPasswordReset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestPasswordResetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/OkResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"429":{"description":"Rate limit exceeded (20/min).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Request a password reset","tags":["Auth"]}},"/auth/password/confirm":{"post":{"description":"Unlike the request step, this reports failure: the caller already holds a token, so saying it is invalid reveals nothing about which accounts exist - and silently claiming success would leave them believing a password changed when it did not.","operationId":"AuthController_confirmPasswordReset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmPasswordResetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/OkResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Complete a password reset","tags":["Auth"]}},"/auth/verify/resend":{"post":{"description":"Always returns `{ ok: true }`, whether or not the address is registered or already verified. Rate limited to 5/min per IP.","operationId":"AuthController_resendVerification","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendEmailVerificationDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/OkResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"429":{"description":"Rate limit exceeded (5/min).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Resend a verification email","tags":["Auth"]}},"/auth/verify/confirm":{"post":{"description":"Redeems the token from the verification link. Single-use and valid for 24 hours. Until this succeeds, `POST /auth/login` refuses the account with 403 `email_unverified`.","operationId":"AuthController_confirmEmailVerification","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmEmailVerificationDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/OkResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Confirm an email address","tags":["Auth"]}},"/grants":{"post":{"description":"Time-boxed, read-only access for an external reviewer. A grant never enters the grantee's write scope, and it stops applying at expiry without anyone revoking it. It does NOT narrow access within the workspace: `scope` is recorded and echoed back but not enforced, so the grantee can read the whole workspace until the grant lapses.","operationId":"GrantsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGrantDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/AccessGrantResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Create a DPCO access grant","tags":["Grants"]},"get":{"description":"Every grant within the caller's scope.","operationId":"GrantsController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AccessGrantResponseDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List grants","tags":["Grants"]}},"/grants/{id}/revoke":{"post":{"description":"Immediate: also kills every live session the grantee holds. Keyed by the grant's own id, so the permission is re-checked against the grant's real workspace once it is loaded.","operationId":"GrantsController_revoke","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/AccessGrantResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such grant within the caller's tenant scope. Also returned when the grant exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Revoke a grant","tags":["Grants"]}},"/roles":{"get":{"description":"Every role, the permissions it grants, and whether it makes MFA mandatory. Derived from the same constants the guards enforce with, so it can never disagree with them.","operationId":"RbacController_roles","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RoleResponseDto"}}}}]}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"The role catalogue","tags":["RBAC"]}},"/permissions":{"get":{"description":"Every permission key with the sentence describing what holding it allows.","operationId":"RbacController_permissions","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PermissionResponseDto"}}}}]}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"The permission catalogue","tags":["RBAC"]}},"/notifications":{"get":{"description":"Newest first. Always the caller's own - there is no way to ask for anybody else's.","operationId":"NotificationsController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationResponseDto"}}}}]}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"The caller's notifications","tags":["Notifications"]}},"/notifications/{id}/read":{"post":{"description":"Answers 404 for an id that is not the caller's, rather than 403: a 403 would confirm the id names a real notification belonging to someone else.","operationId":"NotificationsController_markRead","parameters":[{"name":"id","required":true,"in":"path","schema":{"example":"f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b90","type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/NotificationResponseDto"}}}]}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such notification for this caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Mark one as read","tags":["Notifications"]}},"/users":{"get":{"description":"Everyone holding a role in the named workspace.","operationId":"UsersController_list","parameters":[{"name":"workspaceId","required":true,"in":"query","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceUserDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List users in a workspace","tags":["Users"]}},"/users/invite":{"post":{"description":"Provisions the account if the email is new, then links the role. Refused with 403 if the role would grant a permission the caller does not itself hold in this workspace - so an Org Admin cannot escalate anyone to Super Admin.","operationId":"UsersController_invite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/WorkspaceUserDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Invite a user into a workspace","tags":["Users"]}},"/audit":{"get":{"description":"Every entry within the caller's scope, oldest first. Append-only and hash-chained: entries cannot be modified or deleted through this API or by direct SQL.","operationId":"AuditController_list","parameters":[{"name":"module","required":false,"in":"query","schema":{"example":"workflow","type":"string"}},{"name":"eventType","required":false,"in":"query","schema":{"example":"approval.approved","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntryDto"}}}}]}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"Read the audit trail","tags":["Audit"]}},"/audit/export":{"get":{"description":"Returns a CSV attachment (`audit.csv`) of every entry within the caller's scope.","operationId":"AuditController_export","parameters":[],"responses":{"200":{"description":"A CSV attachment.","content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"Export the audit trail as CSV","tags":["Audit"]}},"/audit/system":{"get":{"description":"The sentinel chain for security events that belong to no workspace (failed logins for unknown emails, and similar). Platform administrators only.","operationId":"AuditController_listSystem","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntryDto"}}}}]}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"Read the system audit chain","tags":["Audit"]}},"/audit/system/export":{"get":{"operationId":"AuditController_exportSystem","parameters":[],"responses":{"200":{"description":"A CSV attachment.","content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"session":[]}],"summary":"Export the system audit chain as CSV","tags":["Audit"]}},"/approvals":{"post":{"description":"Creates a chain with its ordered steps. The maker is the authenticated caller and cannot decide their own chain, so a single-step chain always needs a second user. Requires `approval.decide` in the target workspace.","operationId":"ApprovalsController_createChain","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateChainDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/ApprovalChainResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Open an approval chain","tags":["Approvals"]},"get":{"description":"The approval queue, newest first, each chain carrying its ordered steps so a caller can tell which stage is live and who it waits on without a request per row. Chain status is derived, never stored: rejected once any step rejects, approved once every step approves.","operationId":"ApprovalsController_listChains","parameters":[{"name":"workspaceId","required":true,"in":"query","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalChainResponseDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List the workspace's approval chains","tags":["Approvals"]}},"/approvals/{id}":{"get":{"description":"Returns the chain and its steps in order.","operationId":"ApprovalsController_getChain","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workspaceId","required":true,"in":"query","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/ApprovalChainResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such approval chain within the caller's tenant scope. Also returned when the approval chain exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Read an approval chain","tags":["Approvals"]}},"/approvals/steps/{stepId}/decide":{"post":{"description":"Records an approve/reject on one step and recomputes the chain status. Rejected if: the caller created the chain (separation of duties), the caller is not the step's named approver, the step was already decided, or a lower-order step is still pending. `workspaceId` must be the step's real workspace - the permission checked must be the permission exercised.","operationId":"ApprovalsController_decide","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/DecisionResultDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such approval step within the caller's tenant scope. Also returned when the approval step exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The step was already decided, the chain is no longer pending, or a lower-order step is still pending.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Decide an approval step","tags":["Approvals"]}},"/records/lock":{"post":{"description":"Freezes a snapshot of the record. From here the record is immutable except through `POST /records/amend`.","operationId":"RecordsController_lock","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LockRecordDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/LockedRecordVersionResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The record is already locked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Lock a record at version 1","tags":["Records"]}},"/records/amend":{"post":{"description":"Appends the next version with a reason. Earlier versions are never modified - the database rejects UPDATE and DELETE on version rows outright.","operationId":"RecordsController_amend","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmendRecordDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/LockedRecordVersionResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The record was never locked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Amend a locked record","tags":["Records"]}},"/records":{"get":{"description":"Each locked record once, at its current version. The table stores versions rather than records, so this is the newest row per record. Both `recordType` and `recordId` are returned because neither identifies a record on its own, and the history route needs the pair.","operationId":"RecordsController_list","parameters":[{"name":"workspaceId","required":true,"in":"query","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LockedRecordVersionResponseDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List the workspace's locked records","tags":["Records"]}},"/records/{recordType}/{recordId}/history":{"get":{"description":"Every version in ascending order. Empty if the record is not locked, or is outside the caller's scope.","operationId":"RecordsController_history","parameters":[{"name":"recordType","required":true,"in":"path","schema":{"type":"string"}},{"name":"recordId","required":true,"in":"path","schema":{"type":"string"}},{"name":"workspaceId","required":true,"in":"query","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LockedRecordVersionResponseDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Read a record's amendment history","tags":["Records"]}},"/exceptions":{"post":{"description":"Records a time-boxed, owned acceptance of residual risk. `expiresAt` must be in the future.","operationId":"ExceptionsController_createException","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExceptionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/RiskExceptionResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"File a risk exception","tags":["Risk Exceptions"]},"get":{"description":"Newest first, for the named workspace only.","operationId":"ExceptionsController_listExceptions","parameters":[{"name":"workspaceId","required":true,"in":"query","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RiskExceptionResponseDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List the risk-exception register","tags":["Risk Exceptions"]}},"/exceptions/{id}/reassess":{"post":{"description":"Stamps `reassessedAt`, evidencing that the accepted risk was looked at again before expiry.","operationId":"ExceptionsController_reassess","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRefDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/RiskExceptionResponseDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such risk exception within the caller's tenant scope. Also returned when the risk exception exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Record a reassessment","tags":["Risk Exceptions"]}},"/files":{"post":{"description":"Validates the declared type, enforces a 25 MB cap on the decoded bytes, scans with ClamAV, and only then stores the object and records it. An infected or oversized upload leaves nothing behind in either storage or the database.","operationId":"StorageController_upload","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadFileDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/UploadedFileDto"}}}]}}}},"400":{"description":"The file type is not allowed, the decoded body exceeds 25 MB, or the malware scan flagged the content (`file infected: <signature>`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Upload an evidence file","tags":["Files"]},"get":{"description":"The evidence index, newest first. Returns every file the workspace holds at any scan status, including quarantined ones: a file that failed the scan must stay visible and accountable, not silently disappear. The object key is not returned - a download goes through the download-url route so scope is re-checked at that moment.","operationId":"StorageController_list","parameters":[{"name":"workspaceId","required":true,"in":"query","description":"Must be the file's real workspace. Read scope is plain membership, so this check - not the guard alone - is what stops a member of one workspace pulling a URL for another's file.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FileListItemDto"}}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"List a workspace's evidence files","tags":["Files"]}},"/files/upload-url":{"post":{"description":"Returns a presigned POST into the quarantine bucket and records the file as PENDING_SCAN. The browser POSTs the bytes straight to object storage - they never traverse this API - then calls the confirm route. The upload policy enforces the 25 MB cap and the declared content type at the object store, before a byte is accepted.","operationId":"StorageController_requestUpload","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/PresignedUploadDto"}}}]}}}},"400":{"description":"The file type is not allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Request a presigned direct upload","tags":["Files"]}},"/files/{id}/confirm":{"post":{"description":"Verifies the object actually arrived in quarantine - the client's word is not taken for it - and hands the file to the malware scanner. Idempotent: a repeated call does not queue a second scan. The file stays undownloadable until the scan passes; poll GET /files/:id for the outcome.","operationId":"StorageController_confirmUpload","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/ConfirmedUploadDto"}}}]}}}},"400":{"description":"No uploaded object was found for this file - the upload never completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such file within the caller's tenant scope. Also returned when the file exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Confirm a direct upload and queue its scan","tags":["Files"]}},"/files/{id}/download-url":{"post":{"description":"Returns a URL valid for 300 seconds. A POST rather than a GET because it mints a credential-bearing URL that should not be cached, logged in a query string, or replayed from history.","operationId":"StorageController_downloadUrl","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadUrlDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/SignedUrlDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such file within the caller's tenant scope. Also returned when the file exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Issue a signed download URL","tags":["Files"]}},"/files/{id}":{"get":{"description":"Readable at every status, including the ones that refuse a download. A caller who has just uploaded polls this to learn when the file becomes downloadable, or that it never will.","operationId":"StorageController_status","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workspaceId","required":true,"in":"query","description":"Must be the file's real workspace. Read scope is plain membership, so this check - not the guard alone - is what stops a member of one workspace pulling a URL for another's file.","schema":{"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/FileStatusDto"}}}]}}}},"400":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"No valid session cookie, or the principal holds an MFA-required role and this session has not satisfied MFA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the principal does not hold the required permission in the target workspace, or the workspace is outside the principal's write scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such file within the caller's tenant scope. Also returned when the file exists in another workspace, so the response never confirms an id outside the caller's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"csrf":[]},{"session":[]}],"summary":"Read a file's malware-scan status","tags":["Files"]}},"/health":{"get":{"description":"Whether the process is up. Checks no dependencies by design: a liveness failure restarts the container, and a database blip should not do that. Use `/ready` to decide whether to send traffic.","operationId":"HealthController_liveness","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/LivenessDto"}}}]}}}}},"summary":"Liveness probe","tags":["Health"]}},"/ready":{"get":{"description":"Whether this instance can serve traffic, by probing Postgres, Redis, object storage and the malware scanner. Returns **503** when any dependency is down, so a load balancer can drain the instance without restarting it. Results are cached briefly, and a failure reason is never returned - only up or down.","operationId":"HealthController_readiness","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelopeDto"},{"properties":{"data":{"$ref":"#/components/schemas/ReadinessDto"}}}]}}}}},"summary":"Readiness probe","tags":["Health"]}}},"info":{"title":"Elbamp API","description":"Backend API for **Elbamp**, a Nigeria-first NDPA/NDPC privacy-compliance platform.\n\n### Authentication\n\nAuthentication is a server-side session carried in an `sid` **cookie**, set by\n`POST /auth/login` (or by `POST /auth/mfa/verify` when the account has MFA\nenabled). There is no bearer token.\n\nEvery **mutating** request (POST/PUT/PATCH/DELETE) additionally requires a CSRF\ndouble-submit: send the value of the `csrf` cookie in an `x-csrf-token` header.\nAny GET response will set that cookie.\n\nPrincipals holding a privileged role (Super Admin, Org Admin, DPO, Partner\nAdmin, DPCO Reviewer, Outsourced DPO) **must** satisfy MFA before any route\nother than enrolment and activation will accept their session.\n\n### Tenant isolation\n\nEvery request is authorized server-side against the caller's own scope, and\nPostgres row-level security is the backstop underneath it. A `workspaceId` in a\nrequest body or query is **never** treated as authority - it only names which\nworkspace the permission is checked against. Resources outside the caller's\nscope return **404**, not 403, so a response never confirms the existence of an\nid in another tenant.","version":"1.0.0-m1","contact":{}},"tags":[{"name":"Auth","description":"Registration, login, MFA enrolment and verification, password reset, and session lifecycle. Every other tag's routes require a session minted here."},{"name":"Tenancy","description":"Workspaces, groups and entities - the isolation boundary everything else hangs off. A workspace is one of three modes: ORGANIZATION, PARTNER or HYBRID."},{"name":"Users","description":"Listing and inviting users into a workspace with a role."},{"name":"RBAC","description":"The role and permission catalogue, published so no client reproduces it. Derived from the same constants the guards enforce with."},{"name":"Notifications","description":"The caller's own inbox. A notification belongs to a person rather than a tenant, so these routes are scoped by session and never by workspace."},{"name":"Grants","description":"Time-boxed, revocable read-only access grants for external DPCO reviewers. A grant never confers write access."},{"name":"Audit","description":"The append-only, hash-chained audit trail. Entries can be read and exported, never modified or deleted - the database rejects UPDATE, DELETE and TRUNCATE outright."},{"name":"Approvals","description":"Maker-checker and multi-stage approval chains. A chain's creator can never decide it, steps are decided in order, and a step is decided only by the user or role it names."},{"name":"Records","description":"Record-locking with amendment history. A locked record is immutable: each amendment appends a new version, and version rows cannot be updated or deleted even by direct SQL."},{"name":"Risk Exceptions","description":"The register of accepted, time-boxed deviations from a control, each with an owner and an expiry."},{"name":"Files","description":"Secure evidence storage. Every upload is type-checked, size-capped and malware-scanned before it is stored; downloads are short-lived signed URLs, issued only to callers whose scope covers the file."},{"name":"Health","description":"Liveness."}],"servers":[],"components":{"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"__Host-sid"},"csrf":{"type":"apiKey","in":"header","name":"x-csrf-token"}},"schemas":{"ErrorResponseDto":{"type":"object","properties":{"success":{"type":"boolean","example":false,"enum":[false]},"message":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"missing required permission"},"data":{"type":"object","additionalProperties":false,"nullable":true,"example":null,"description":"Always null on a failure."}},"required":["success","message","data"]},"SuccessEnvelopeDto":{"type":"object","properties":{"success":{"type":"boolean","example":true,"enum":[true]},"message":{"type":"string","example":"Approval step decided.","description":"Human-readable and safe to surface directly in a UI."}},"required":["success","message"]},"WorkspaceResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"mode":{"type":"string","enum":["ORGANIZATION","PARTNER","HYBRID"],"example":"ORGANIZATION","description":"ORGANIZATION: a single company. PARTNER: a firm managing client workspaces. HYBRID: both at once."},"name":{"type":"string","example":"Acme Nigeria Ltd"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"updatedAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"deletedAt":{"type":"object","example":null,"nullable":true,"format":"date-time"}},"required":["id","mode","name","createdAt","updatedAt","deletedAt"]},"BranchResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"entityId":{"type":"string","example":"c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f"},"name":{"type":"string","example":"Lagos","description":"Branch, campus, region or unit."},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"updatedAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"deletedAt":{"type":"object","example":null,"nullable":true,"format":"date-time","description":"Soft-delete marker. Always null in the structure listing, which filters deleted rows out."}},"required":["id","workspaceId","entityId","name","createdAt","updatedAt","deletedAt"]},"EntityWithBranchesDto":{"type":"object","properties":{"id":{"type":"string","example":"c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"groupId":{"type":"string","example":"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e","description":"The parent group. Enforced by a composite foreign key on (groupId, workspaceId), so an entity can never be attached to a group in another tenant."},"name":{"type":"string","example":"Acme Nigeria Ltd"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"updatedAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"deletedAt":{"type":"object","example":null,"nullable":true,"format":"date-time","description":"Soft-delete marker. Always null in the structure listing, which filters deleted rows out."},"branches":{"type":"array","items":{"$ref":"#/components/schemas/BranchResponseDto"}}},"required":["id","workspaceId","groupId","name","createdAt","updatedAt","deletedAt","branches"]},"GroupWithEntitiesDto":{"type":"object","properties":{"id":{"type":"string","example":"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"name":{"type":"string","example":"Acme Holdings"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"updatedAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"deletedAt":{"type":"object","example":null,"nullable":true,"format":"date-time","description":"Soft-delete marker. Always null in the structure listing, which filters deleted rows out."},"entities":{"type":"array","items":{"$ref":"#/components/schemas/EntityWithBranchesDto"}}},"required":["id","workspaceId","name","createdAt","updatedAt","deletedAt","entities"]},"GroupResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"name":{"type":"string","example":"Acme Holdings"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"updatedAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"deletedAt":{"type":"object","example":null,"nullable":true,"format":"date-time","description":"Soft-delete marker. Always null in the structure listing, which filters deleted rows out."}},"required":["id","workspaceId","name","createdAt","updatedAt","deletedAt"]},"CreateGroupDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"name":{"type":"string","example":"Acme Holdings","description":"The holding company at the top of a group hierarchy."}},"required":["workspaceId","name"]},"EntityResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"groupId":{"type":"string","example":"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e","description":"The parent group. Enforced by a composite foreign key on (groupId, workspaceId), so an entity can never be attached to a group in another tenant."},"name":{"type":"string","example":"Acme Nigeria Ltd"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"updatedAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"deletedAt":{"type":"object","example":null,"nullable":true,"format":"date-time","description":"Soft-delete marker. Always null in the structure listing, which filters deleted rows out."}},"required":["id","workspaceId","groupId","name","createdAt","updatedAt","deletedAt"]},"CreateEntityDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"name":{"type":"string","example":"Acme Nigeria Ltd","description":"A subsidiary or legal entity within the group."}},"required":["workspaceId","name"]},"CreateBranchDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"name":{"type":"string","example":"Lagos Campus","description":"A branch, campus, region or unit belonging to the entity."}},"required":["workspaceId","name"]},"RegisterResponseDto":{"type":"object","properties":{"userId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"Registration always mints the registrant a fresh ORGANIZATION workspace and makes them its Org Admin."}},"required":["userId","workspaceId"]},"RegisterDto":{"type":"object","properties":{"email":{"type":"string","example":"dpo@acme.ng","format":"email"},"password":{"type":"string","example":"pw12345!","minLength":8},"fullName":{"type":"string","example":"Adaeze Okonkwo","minLength":2,"maxLength":120},"organizationName":{"type":"string","example":"Acme Nigeria Ltd","minLength":2,"maxLength":120,"description":"Names the workspace this registration mints, and its Organization record."}},"required":["email","password","fullName","organizationName"]},"LoginResponseDto":{"type":"object","properties":{"status":{"type":"string","enum":["ok","mfa_required"],"example":"mfa_required"},"challengeId":{"type":"string","example":"c9f2a7b1-3d4e-4f5a-8b6c-7d8e9f0a1b2c","description":"Present only when status is `mfa_required`. Single-use and short-lived."}},"required":["status"]},"LoginDto":{"type":"object","properties":{"email":{"type":"string","example":"dpo@acme.ng","format":"email"},"password":{"type":"string","example":"pw12345!","description":"No minimum length here: a login is a credential CHECK, so a short guess must still reach the 401 path rather than be rejected as a 400."}},"required":["email","password"]},"PrincipalRoleDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"roleKey":{"type":"string","example":"ORG_ADMIN"}},"required":["workspaceId","roleKey"]},"MfaStatusDto":{"type":"object","properties":{"enabled":{"type":"boolean","example":false,"description":"Whether this account has enrolled."},"required":{"type":"boolean","example":true,"description":"Whether any role the caller holds makes MFA mandatory."},"dueAt":{"type":"object","nullable":true,"example":"2026-08-26T09:00:00.000Z","description":"When MFA becomes a hard block for this account. Null when no countdown applies - either no privileged role, or MFA is already on."},"daysRemaining":{"type":"object","nullable":true,"example":14,"description":"Whole days left, floored at 0. Null whenever `dueAt` is null."}},"required":["enabled","required"]},"MeResponseDto":{"type":"object","properties":{"userId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"email":{"type":"string","example":"dpo@acme.ng","format":"email"},"fullName":{"type":"string","example":"Adaeze Okonkwo"},"workspaceIds":{"example":["0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"],"description":"Every workspace the caller can read. Write access is narrower and permission-derived.","type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"$ref":"#/components/schemas/PrincipalRoleDto"}},"mfa":{"$ref":"#/components/schemas/MfaStatusDto"}},"required":["userId","email","fullName","workspaceIds","roles","mfa"]},"OkResponseDto":{"type":"object","properties":{"ok":{"type":"boolean","example":true}},"required":["ok"]},"EnrollMfaResponseDto":{"type":"object","properties":{"secret":{"type":"string","example":"JBSWY3DPEHPK3PXP","description":"The base32 TOTP secret. Returned once, at enrolment, and never again."},"otpauthUrl":{"type":"string","example":"otpauth://totp/Elbamp:dpo@acme.ng?secret=JBSWY3DPEHPK3PXP&issuer=Elbamp","description":"Render as a QR code for the authenticator app."}},"required":["secret","otpauthUrl"]},"EnrollMfaDto":{"type":"object","properties":{"currentPassword":{"type":"string","example":"pw12345!","description":"Step-up, required only when re-enrolling an account that already has MFA on."},"currentToken":{"type":"string","example":"123456","description":"Step-up alternative to currentPassword, for re-enrolment."}}},"ActivateMfaResponseDto":{"type":"object","properties":{"recoveryCodes":{"example":["a1b2c3d4e5","f6a7b8c9d0"],"description":"Single-use recovery codes, shown once. Activation also rotates the caller onto a fresh MFA-satisfied session and revokes every other session they held, so a stolen pre-activation session cannot outlive activation.","type":"array","items":{"type":"string"}}},"required":["recoveryCodes"]},"ActivateMfaDto":{"type":"object","properties":{"token":{"type":"string","example":"123456","description":"A current TOTP code proving the authenticator is set up correctly."},"currentPassword":{"type":"string","example":"pw12345!","description":"Step-up, required only when reactivating an account that already has MFA on."}},"required":["token"]},"VerifyMfaDto":{"type":"object","properties":{"challengeId":{"type":"string","example":"c9f2a7b1-3d4e-4f5a-8b6c-7d8e9f0a1b2c","description":"From the `mfa_required` login response. Single-use."},"token":{"type":"string","example":"123456","description":"A TOTP code, or one of the single-use recovery codes issued at activation."}},"required":["challengeId","token"]},"RequestPasswordResetDto":{"type":"object","properties":{"email":{"type":"string","example":"dpo@acme.ng","format":"email"}},"required":["email"]},"ConfirmPasswordResetDto":{"type":"object","properties":{"token":{"type":"string","example":"8f3a...","description":"The token from the reset link."},"newPassword":{"type":"string","example":"newpw12345!","minLength":8}},"required":["token","newPassword"]},"ResendEmailVerificationDto":{"type":"object","properties":{"email":{"type":"string","example":"dpo@acme.ng","format":"email"}},"required":["email"]},"ConfirmEmailVerificationDto":{"type":"object","properties":{"token":{"type":"string","example":"9f3c1b7a2d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8","description":"The token from the verification link. Single-use, valid for 24 hours."}},"required":["token"]},"AccessGrantResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"granteeUserId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"scope":{"type":"object","additionalProperties":true,"example":{"departments":["legal"],"categories":["dsr"]},"description":"NOT ENFORCED - recorded and echoed back, but nothing narrows access by it. See CreateGrantDto.scope."},"expiresAt":{"type":"string","example":"2026-12-31T23:59:59.000Z","format":"date-time"},"revokedAt":{"type":"object","example":null,"nullable":true,"format":"date-time","description":"Set by revoke, which also kills every live session the grantee holds."},"revokedBy":{"type":"object","example":null,"nullable":true,"description":"Who revoked it. Taken from the session, never the request body. Null until revoked."},"createdBy":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","description":"Who granted it. Taken from the session, never the request body."},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"}},"required":["id","workspaceId","granteeUserId","scope","expiresAt","revokedAt","revokedBy","createdBy","createdAt"]},"CreateGrantDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"granteeEmail":{"type":"string","example":"reviewer@dpco.ng","format":"email","description":"Provisioned if they do not already exist."},"scope":{"type":"object","additionalProperties":true,"example":{"departments":["legal"],"categories":["dsr"]},"description":"NOT ENFORCED. Recorded on the grant and returned verbatim, but nothing reads it: a grant is read access to the WHOLE workspace for its lifetime, whatever this contains. Treat it as documentation of intent, not as a boundary - do not build a UI that implies the reviewer will only see these departments or categories. What a grant does enforce is read-only (it never enters the write scope) and time-boxed (it stops applying at expiresAt)."},"expiresAt":{"type":"string","example":"2026-12-31T23:59:59.000Z","format":"date-time","description":"Grants auto-expire: the principal stops carrying an expired grant without anyone revoking it."}},"required":["workspaceId","granteeEmail","expiresAt"]},"RoleResponseDto":{"type":"object","properties":{"key":{"type":"string","example":"ORG_ADMIN"},"name":{"type":"string","example":"Organization Admin"},"permissions":{"example":["workspace.read","user.invite"],"type":"array","items":{"type":"string"}},"mfaRequired":{"type":"boolean","example":true,"description":"Whether holding this role makes MFA mandatory, after the grace period."}},"required":["key","name","permissions","mfaRequired"]},"PermissionResponseDto":{"type":"object","properties":{"key":{"type":"string","example":"audit.export"},"description":{"type":"string","example":"Export the audit trail"}},"required":["key","description"]},"NotificationResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b90"},"userId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"workspaceId":{"type":"object","nullable":true,"example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"Which workspace this is about, when it is about one. A notification belongs to a person, not a tenant."},"type":{"type":"string","example":"mfa_reminder"},"title":{"type":"string","example":"Set up two-factor authentication within the next week"},"body":{"type":"string","example":"Your role requires two-factor authentication. 7 day(s) remain of the 14-day setup window."},"readAt":{"type":"object","nullable":true,"example":null,"format":"date-time"},"createdAt":{"type":"string","example":"2026-08-12T09:10:00.000Z","format":"date-time"}},"required":["id","userId","type","title","body","createdAt"]},"WorkspaceUserDto":{"type":"object","properties":{"userId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"email":{"type":"string","example":"custodian@acme.ng","format":"email"},"fullName":{"type":"string","example":"Chinua Achebe"},"roleKey":{"type":"string","example":"CUSTODIAN"}},"required":["userId","email","fullName","roleKey"]},"InviteUserDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"email":{"type":"string","example":"custodian@acme.ng","format":"email","description":"An account is provisioned if this email is new."},"fullName":{"type":"string","example":"Chinua Achebe","minLength":2,"maxLength":120,"description":"Recorded only when this email is new. An existing account keeps the name it already has - an invite attaches a role, it does not rename anyone."},"roleKey":{"type":"string","enum":["SUPER_ADMIN","ORG_ADMIN","DPO","CUSTODIAN","MANAGEMENT","PARTNER_ADMIN","DPCO_REVIEWER","OUTSOURCED_DPO","LIGHT_CLIENT"],"example":"CUSTODIAN","description":"You cannot assign a role granting permissions you do not yourself hold in this workspace."}},"required":["workspaceId","email","fullName","roleKey"]},"AuditLogEntryDto":{"type":"object","properties":{"id":{"type":"string","example":"e5f6a7b8-c9d0-4e1f-8a2b-4c5d6e7f8091"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"seq":{"type":"number","example":42,"description":"Position in this workspace's chain. Gapless and strictly increasing."},"eventType":{"type":"string","example":"approval.approved"},"module":{"type":"string","example":"workflow"},"actorUserId":{"type":"object","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","nullable":true},"actorRole":{"type":"object","example":"DPO","nullable":true},"object":{"type":"object","example":"8c4d1e2f-5a6b-4c7d-9e0f-1a2b3c4d5e6f","nullable":true},"oldValue":{"type":"object","additionalProperties":true,"nullable":true,"example":null},"newValue":{"type":"object","additionalProperties":true,"nullable":true,"example":{"stepId":"3f2b9c40-7c1a-4f0e-9d3b-2a6f1c8e5b71","decision":"approved","chainStatus":"approved"}},"ip":{"type":"object","example":"102.89.23.7","nullable":true},"userAgent":{"type":"object","example":"Mozilla/5.0","nullable":true},"prevHash":{"type":"string","example":"0000000000000000000000000000000000000000000000000000000000000000","description":"The previous entry's hash; all zeroes for the first entry in a workspace."},"hash":{"type":"string","example":"9f2c1b7e4a8d3f60c5e2b1a09d8c7f6e5d4c3b2a1908f7e6d5c4b3a2918f7e6d","description":"sha256 over the previous hash plus this entry's canonical form, including createdAt."},"createdAt":{"type":"string","example":"2026-08-05T09:12:44.000Z","format":"date-time"}},"required":["id","workspaceId","seq","eventType","module","actorUserId","actorRole","object","oldValue","newValue","ip","userAgent","prevHash","hash","createdAt"]},"ApprovalStepResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"3f2b9c40-7c1a-4f0e-9d3b-2a6f1c8e5b71"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"chainId":{"type":"string","example":"8c4d1e2f-5a6b-4c7d-9e0f-1a2b3c4d5e6f"},"order":{"type":"number","example":1,"description":"Decided in ascending order; a step is blocked while any lower order is still pending."},"approverRole":{"type":"object","example":"DPO","nullable":true,"description":"The role permitted to decide this step, if it is role-addressed."},"approverUserId":{"type":"object","example":null,"nullable":true,"description":"The exact user permitted to decide this step. Takes precedence over approverRole."},"status":{"type":"string","enum":["pending","approved","rejected"],"example":"approved"},"decidedBy":{"type":"object","example":"b7e1c2a3-4d5f-4a6b-8c9d-0e1f2a3b4c5d","nullable":true},"comment":{"type":"object","example":"Evidence is sufficient.","nullable":true},"decidedAt":{"type":"object","example":"2026-08-05T09:12:44.000Z","nullable":true,"format":"date-time"}},"required":["id","workspaceId","chainId","order","approverRole","approverUserId","status","decidedBy","comment","decidedAt"]},"ApprovalChainResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"8c4d1e2f-5a6b-4c7d-9e0f-1a2b3c4d5e6f"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"subjectType":{"type":"string","example":"evidence","description":"The kind of thing being approved. Opaque to the engine."},"subjectId":{"type":"string","example":"ev_2f8a91","description":"The id of the thing being approved, in its own module's namespace."},"status":{"type":"string","enum":["pending","approved","rejected"],"example":"pending","description":"Derived, never set directly: rejected if any step rejected, approved once every step has approved, otherwise pending."},"createdBy":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","description":"The maker. Taken from the session, never the request body - and barred from deciding this chain."},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalStepResponseDto"}}},"required":["id","workspaceId","subjectType","subjectId","status","createdBy","createdAt","steps"]},"ApprovalStepDto":{"type":"object","properties":{"order":{"type":"number","example":1,"description":"Steps are decided in ascending order."},"approverRole":{"type":"string","example":"DPO","description":"Any user holding this role in the workspace may decide the step. Ignored if approverUserId is set."},"approverUserId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","description":"Only this user may decide the step. Takes precedence over approverRole."}},"required":["order"]},"CreateChainDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own."},"subjectType":{"type":"string","example":"evidence","description":"The kind of thing being approved. Opaque to the engine."},"subjectId":{"type":"string","example":"ev_2f8a91","description":"The id of the thing being approved, in its own module's namespace."},"steps":{"minItems":1,"description":"At least one step. There is no createdBy field: the maker is always the authenticated caller.","type":"array","items":{"$ref":"#/components/schemas/ApprovalStepDto"}}},"required":["workspaceId","subjectType","subjectId","steps"]},"DecisionResultDto":{"type":"object","properties":{"chainStatus":{"type":"string","enum":["pending","approved","rejected"],"example":"approved","description":"The chain's status after this decision - still pending if further steps remain."}},"required":["chainStatus"]},"DecideDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own."},"decision":{"type":"string","enum":["approved","rejected"],"example":"approved"},"comment":{"type":"string","example":"Evidence is sufficient."}},"required":["workspaceId","decision"]},"LockedRecordVersionResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"3f2b9c40-7c1a-4f0e-9d3b-2a6f1c8e5b71"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"recordType":{"type":"string","example":"report"},"recordId":{"type":"string","example":"rep_2026Q2"},"version":{"type":"number","example":2,"description":"1 on lock; each amendment appends the next version. Earlier versions are never modified."},"snapshot":{"type":"object","additionalProperties":true,"example":{"title":"Q2 readiness report","score":82},"description":"The full record body as at this version."},"reason":{"type":"object","example":"Corrected the vendor count.","nullable":true,"description":"Null on version 1; required on every amendment."},"createdBy":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"}},"required":["id","workspaceId","recordType","recordId","version","snapshot","reason","createdBy","createdAt"]},"LockRecordDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own."},"recordType":{"type":"string","example":"report"},"recordId":{"type":"string","example":"rep_2026Q2"},"snapshot":{"type":"object","additionalProperties":true,"example":{"title":"Q2 readiness report","score":82},"description":"The full record body to freeze at this version."}},"required":["workspaceId","recordType","recordId","snapshot"]},"AmendRecordDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own."},"recordType":{"type":"string","example":"report"},"recordId":{"type":"string","example":"rep_2026Q2"},"snapshot":{"type":"object","additionalProperties":true,"example":{"title":"Q2 readiness report","score":82},"description":"The full record body to freeze at this version."},"reason":{"type":"string","example":"Corrected the vendor count.","description":"Why the locked record is being amended. Required - the amendment history is the audit artefact."}},"required":["workspaceId","recordType","recordId","snapshot","reason"]},"RiskExceptionResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"3f2b9c40-7c1a-4f0e-9d3b-2a6f1c8e5b71"},"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"title":{"type":"string","example":"Legacy vendor without a DPA"},"affected":{"type":"string","example":"vendor-X","description":"What the exception applies to."},"residualRisk":{"type":"string","example":"medium"},"compensatingControls":{"type":"object","example":"Quarterly manual review; no personal data in scope.","nullable":true},"ownerUserId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"approvedBy":{"type":"object","example":null,"nullable":true},"expiresAt":{"type":"string","example":"2026-11-30T00:00:00.000Z","format":"date-time","description":"Must be in the future at creation."},"reassessedAt":{"type":"object","example":null,"nullable":true,"format":"date-time"},"status":{"type":"string","enum":["open","closed"],"example":"open"},"createdAt":{"type":"string","example":"2026-08-05T09:10:00.000Z","format":"date-time"}},"required":["id","workspaceId","title","affected","residualRisk","compensatingControls","ownerUserId","approvedBy","expiresAt","reassessedAt","status","createdAt"]},"CreateExceptionDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own."},"title":{"type":"string","example":"Legacy vendor without a DPA"},"affected":{"type":"string","example":"vendor-X","description":"What the exception applies to."},"residualRisk":{"type":"string","example":"medium"},"compensatingControls":{"type":"string","example":"Quarterly manual review; no personal data in scope."},"ownerUserId":{"type":"string","example":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","description":"Who owns bringing this exception to a close."},"expiresAt":{"format":"date-time","type":"string","example":"2026-11-30T00:00:00.000Z","description":"Must be in the future: an exception that is already expired when filed would never surface on an expiry sweep."}},"required":["workspaceId","title","affected","residualRisk","ownerUserId","expiresAt"]},"WorkspaceRefDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"The workspace this request acts in. Names which workspace the permission is checked against - never authority on its own."}},"required":["workspaceId"]},"UploadedFileDto":{"type":"object","properties":{"id":{"type":"string","example":"7d9c2a10-4b6e-4f81-9c33-8e5a1d0f6b24"},"key":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52/3f2b9c40-7c1a-4f0e-9d3b-2a6f1c8e5b71-policy.pdf","description":"The object key. Always prefixed with the workspace id, and the filename is sanitized, so a name like `../../etc/passwd` cannot escape the prefix."}},"required":["id","key"]},"UploadFileDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"filename":{"type":"string","example":"policy.pdf","description":"Sanitized before it becomes part of the object key."},"contentType":{"type":"string","example":"application/pdf","description":"Must be one of: application/pdf, image/png, image/jpeg, text/csv, and the OpenXML spreadsheet/document types. Caller-declared rather than sniffed, so this is a hint - the malware scan is the real backstop."},"body":{"type":"string","format":"byte","example":"Y2xlYW4gcGRmIGJ5dGVz","description":"The file, base64-encoded. The 25 MB cap applies to the DECODED bytes; base64 inflates the request body by roughly a third."}},"required":["workspaceId","filename","contentType","body"]},"PresignedUploadDto":{"type":"object","properties":{"fileId":{"type":"string","example":"7d9c2a10-4b6e-4f81-9c33-8e5a1d0f6b24"},"url":{"type":"string","example":"https://s3.example.com/elbamp-quarantine","description":"POST the multipart form here. Not the evidence bucket - uploads land in quarantine until scanned."},"fields":{"type":"object","additionalProperties":{"type":"string"},"description":"Send every field verbatim, in the order given, with the file appended LAST. They include the signed policy, so altering any of them invalidates the upload."},"expiresIn":{"type":"number","example":300,"description":"Seconds the presigned POST stays valid."}},"required":["fileId","url","fields","expiresIn"]},"RequestUploadDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52"},"filename":{"type":"string","example":"policy.pdf","description":"Sanitized before it becomes part of the object key; the key itself is minted server-side."},"contentType":{"type":"string","example":"application/pdf","description":"Must be one of the allowed types. Also signed into the upload policy, so the object store rejects a body that does not match it."}},"required":["workspaceId","filename","contentType"]},"ConfirmedUploadDto":{"type":"object","properties":{"id":{"type":"string","example":"7d9c2a10-4b6e-4f81-9c33-8e5a1d0f6b24"},"status":{"type":"string","enum":["PENDING_SCAN","CLEAN","INFECTED"],"example":"PENDING_SCAN","description":"PENDING_SCAN on a first confirmation - the scan runs in the background. Poll GET /files/:id for the outcome."}},"required":["id","status"]},"ConfirmUploadDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"Must be the file's real workspace, checked against the row rather than trusted."}},"required":["workspaceId"]},"SignedUrlDto":{"type":"object","properties":{"url":{"type":"string","example":"http://localhost:9000/elbamp-evidence/0a1e5d6c.../3f2b9c40...-policy.pdf?X-Amz-Signature=...","description":"A pre-signed URL valid for 300 seconds. Treat it as a credential."}},"required":["url"]},"DownloadUrlDto":{"type":"object","properties":{"workspaceId":{"type":"string","example":"0a1e5d6c-9b2f-4a83-bd47-1c0e9f3a7d52","description":"Must be the file's real workspace. Read scope is plain membership, so this check - not the guard alone - is what stops a member of one workspace pulling a URL for another's file."}},"required":["workspaceId"]},"FileListItemDto":{"type":"object","properties":{"id":{"type":"string","example":"7d9c2a10-4b6e-4f81-9c33-8e5a1d0f6b24"},"status":{"type":"string","enum":["PENDING_SCAN","CLEAN","INFECTED"],"example":"CLEAN"},"filename":{"type":"string","example":"policy.pdf"},"contentType":{"type":"string","example":"application/pdf"},"createdAt":{"format":"date-time","type":"string","example":"2026-08-14T09:12:04.000Z"},"createdBy":{"type":"object","example":"3f2b8c11-77aa-4e0d-9a6b-1c5e2f8d4b90","nullable":true,"description":"The uploader, when one is recorded."}},"required":["id","status","filename","contentType","createdAt","createdBy"]},"FileStatusDto":{"type":"object","properties":{"id":{"type":"string","example":"7d9c2a10-4b6e-4f81-9c33-8e5a1d0f6b24"},"status":{"type":"string","enum":["PENDING_SCAN","CLEAN","INFECTED"],"example":"CLEAN","description":"Only a CLEAN file yields a download URL. PENDING_SCAN means the bytes are in storage but the scanner has not finished; INFECTED means it finished and the file will never be served."},"filename":{"type":"string","example":"policy.pdf"},"contentType":{"type":"string","example":"application/pdf"}},"required":["id","status","filename","contentType"]},"LivenessDto":{"type":"object","properties":{"status":{"type":"string","enum":["ok"],"example":"ok"},"version":{"type":"string","example":"1.0.0-m1"},"uptimeSeconds":{"type":"number","example":3600,"description":"Seconds since this process started."}},"required":["status","version","uptimeSeconds"]},"DependencyCheckDto":{"type":"object","properties":{"name":{"type":"string","enum":["postgres","redis","storage","scanner"],"example":"postgres"},"status":{"type":"string","enum":["up","down"],"example":"up","description":"Only up or down. The underlying error is logged server-side but never returned: this route is public, and a driver error can carry a hostname, port or schema detail."},"latencyMs":{"type":"number","example":3,"description":"How long the probe took. A probe over 2s is reported down."}},"required":["name","status","latencyMs"]},"ReadinessDto":{"type":"object","properties":{"status":{"type":"string","enum":["ready","degraded"],"example":"ready","description":"`degraded` if any single dependency is down. The HTTP status is then 503."},"checks":{"type":"array","items":{"$ref":"#/components/schemas/DependencyCheckDto"}},"uptimeSeconds":{"type":"number","example":3600},"version":{"type":"string","example":"1.0.0-m1"}},"required":["status","checks","uptimeSeconds","version"]}}}}