{"openapi":"3.0.3","info":{"title":"EnforceAuth API","description":"The EnforceAuth platform API powers a national-scale policy enforcement service.\nIt provides endpoints for managing organizational hierarchies, OPA/EOPA policy deployments,\ndecision log ingestion and replay, CI/CD workload identity, and billing.\n\nAll tenant-facing endpoints enforce multi-tenant isolation via auth-context-derived `tenant_id`\n(from JWT, API key, or OIDC token exchange).\nState-changing operations support idempotency via the `Idempotency-Key` header.\n\n## Authentication\n\n| Method | Header | Use Case |\n|--------|--------|----------|\n| **Bearer JWT** | `Authorization: Bearer <token>` | UI and user-facing API calls. Validated by a custom authorizer. |\n| **API Key** | `X-API-Key` | Machine-to-machine ingestion (OPA status, decision logs). Scoped per-entity with explicit permissions. |\n| **Workload Identity (OIDC)** | RFC 8693 token exchange | CI/CD pipelines. Exchanges a CI provider OIDC token for a short-lived EnforceAuth access token. |\n| **AWS IAM (Sigv4)** | AWS Signature V4 | Internal service-to-service routes only. |\n\n## Infrastructure\n\nThe API is designed for high availability, durability, and compliance:\n- Decision logs are ingested into a streaming pipeline and stored with write-once-read-many\n  (WORM) compliance in production.\n- Policy bundles are compiled and deployed automatically via an event-driven pipeline.\n- Policy deployments can be triggered automatically via git webhooks (see the Webhooks tag)\n  or manually via `POST /v1/entities/{id}/policies/deploy`.\n","version":"v1.156.1","contact":{"name":"EnforceAuth Team","url":"https://enforceauth.com","email":"support@enforceauth.com"}},"servers":[{"url":"https://localhost:3001","description":"Current"},{"url":"https://api.enforceauth.dev","description":"Development"},{"url":"https://api.enforceauth.com","description":"Production"}],"tags":[{"name":"Info","description":"API information and health check endpoints"},{"name":"Invitations","description":"User invitation management"},{"name":"Internal","description":"Internal service-to-service endpoints secured by IAM authorization — not exposed to external\nconsumers. These endpoints are used by internal pipeline services (e.g., the policy deployment\npipeline, billing reconciliation) and require AWS Sigv4 signed requests.\n"},{"name":"Admin","description":"Administrative operations for platform management"},{"name":"OIDC","description":"OpenID Connect token exchange for CI/CD workload identity.\nImplements RFC 8693 Token Exchange: a CI runner (GitHub Actions, GitLab CI, etc.) presents\nits native OIDC token, and EnforceAuth issues a short-lived access token scoped to a\nspecific entity and set of permissions. The flow is:\n1. CI job obtains an OIDC token from the provider (e.g., `ACTIONS_ID_TOKEN_REQUEST_TOKEN`).\n2. CI job calls `POST /v1/auth/oidc/token` with the token and target `entity_id`.\n3. EnforceAuth validates the token against registered CI Providers and Trust Policies.\n4. If a matching trust policy exists, an access token is returned with the granted scopes.\nThis enables keyless authentication for CI/CD pipelines — no long-lived API keys required.\n"},{"name":"CI Providers","description":"CI/CD provider registration and management. A CI Provider represents an external identity\nprovider whose OIDC tokens are trusted for workload identity (e.g., GitHub Actions, GitLab CI).\nEach provider has an `issuer_url` and `audience` used to validate incoming tokens during\nthe OIDC token exchange flow. Providers are tenant-scoped and can be enabled/disabled.\n"},{"name":"Status","description":"OPA/EOPA instance status reporting and fleet health. OPA agents send periodic status reports\nvia `POST /v1/status` using an API key with `status:write` scope. These reports include\nbundle load status, plugin health, and instance labels. The data powers the fleet health\ndashboard and monitoring metrics.\n"},{"name":"Monitoring","description":"Fleet monitoring metrics and error tracking. Aggregates OPA status data into time-series metrics and error summaries for the monitoring dashboard."},{"name":"Trust Policies","description":"OIDC trust policy management for CI/CD providers. Trust policies define the claim-matching\nrules that determine which CI tokens are accepted and what scopes they receive. Each policy\nmaps a CI Provider + subject pattern (e.g., `repo:org/repo:ref:refs/heads/main`) to a set\nof scopes (e.g., `pipeline:deploy`, `pipeline:read`) on a specific entity.\n"},{"name":"Bootstrap","description":"First-time platform setup and initialization. Creates the root organization entity and assigns the bootstrapping user as owner."},{"name":"Users","description":"User profile and organization membership"},{"name":"Entities","description":"Entity hierarchy management (organizations, systems, teams). Entities form a directed acyclic graph (DAG) that drives configuration inheritance and policy deployment fan-out."},{"name":"Configs","description":"Entity configuration management with inheritance. Configs (e.g., `policy_source`,\n`bundle_destination`, `environment_config`) are attached to entities and resolved by walking the entity hierarchy.\nThe policy deployment pipeline reads effective configs to determine where to fetch policies\nfrom and where to upload compiled bundles.\n"},{"name":"Promotions","description":"Environment drift detection and promotion event APIs."},{"name":"Policy Files","description":"Policy file tree and content viewing. Reads the OPA/Rego policy file tree from the configured git repository for a given entity."},{"name":"Policies","description":"Policy deployment triggers and pipeline logs. `POST /v1/entities/{id}/policies/deploy`\ntriggers the deployment pipeline:\n1. A deployment run record is created (status: `pending`).\n2. The policy pipeline is triggered asynchronously.\n3. The pipeline fetches policies from git, compiles an OPA bundle, runs tests, and uploads\n   the bundle to the configured destination.\n4. Deployment status is updated as each phase completes\n   (`init → fetch → test → build → deploy → complete`).\nThe `scope=descendants` option fans out deployment to all descendant entities.\n"},{"name":"Secrets","description":"Secret storage, rotation, and credential management. Secrets (e.g., git tokens, bundle\ndestination credentials) are stored securely and referenced by entity. The policy deployment\npipeline retrieves secrets at runtime to authenticate with external systems (GitHub, S3, etc.).\n"},{"name":"Decisions","description":"Decision log ingestion, querying, and replay. OPA/EOPA agents send decision logs via\n`POST /v1/logs` using an API key with `decisions:write` scope. Logs are processed\nthrough a streaming pipeline and stored durably, partitioned by tenant and date.\nThe log replay endpoints enable policy\nbacktesting against historical decision data.\n"},{"name":"Deployments","description":"Policy bundle deployment tracking and management. Each deployment run tracks the full\nlifecycle: `pending → in_progress → success/failed/timeout`. The pipeline progresses\nthrough phases: `init → fetch → test → build → deploy → complete`. Org-level fan-out\ndeployments share a `group_id` for coordinated tracking.\n"},{"name":"Webhooks","description":"Webhook endpoints for external integrations. The policy change webhook\n(`POST /v1/webhooks/policy-change`) receives push events from GitHub or GitLab when the\npolicy repository changes. It validates the `X-Hub-Signature-256` header (GitHub) or\nequivalent, identifies affected entities by matching the repository URL to configured\n`policy_source` configs, and triggers deployment pipelines for each matched entity.\n"},{"name":"Documentation","description":"API documentation and OpenAPI spec endpoints. Serves a Redoc-powered interactive documentation page and the raw OpenAPI 3.0 JSON spec."},{"name":"API Keys","description":"Create and manage API keys for programmatic access. Keys are scoped to a specific entity\nand carry explicit permission scopes (e.g., `decisions:write`, `status:write`). API keys\nenable machine-to-machine authentication for OPA agents and CI/CD pipelines without\nrequiring a JWT. Keys are securely hashed and can be revoked at any time.\n"},{"name":"Feature Flags","description":"In-house feature flag service (EA-906) that replaces Flagsmith. Three audiences:\n`GET /v1/flags` evaluates product flags for the authenticated caller (fail-closed,\nalways returns 200); `GET /v1/public-flags?scope=marketing` serves the marketing-site\npage-visibility map unauthenticated and edge-cached for 30 seconds; the admin CRUD at\n`/v1/feature-flags/admin/**` is PM-admin-only for global defaults and customer-admin-only\nfor per-tenant overrides. Evaluation precedence is user override > tenant override >\nglobal default.\n"},{"name":"Permissions","description":"Role-based access control and permission checking. Permissions are enforced by an embedded\nOPA policy engine (fail-closed). Actions follow the `resource:action` format\n(e.g., `entities:read`, `policies:deploy`).\n"},{"name":"Audit","description":"Audit log querying and export. Audit events are written for all state-changing operations\nand retained with long-term archival for compliance.\n"},{"name":"Dashboard","description":"Dashboard metrics and analytics. Aggregates bundle deployment health, OPA fleet status, and decision statistics for the tenant's monitoring dashboard."},{"name":"Assistant","description":"AI assistant conversation and tool execution endpoints"},{"name":"Billing","description":"Billing profiles, usage metering, and payment management"},{"name":"User Groups","description":"Workforce segmentation primitive for Functionality Management (EA-868). Client Admins create\nand manage user groups within their tenant; each group is the unit of granularity for Feature\naccess control. A user with no group membership has zero access. Names are case-insensitive\nand unique per tenant.\n"},{"name":"Integrations","description":"Per-tenant third-party integrations (EA-687). Currently covers GitHub App installations\nused as a tier-2 credential source for `policy_source` git access. The install flow is\nOAuth-style: the frontend redirects the user (`GET /v1/integrations/github/install/start`)\nto GitHub's installation prompt with a signed state token; GitHub redirects back to\n`GET /v1/integrations/github/install/callback` with the `installation_id`. The callback is\npublic (no session) — the signed state token IS the tenant binding and provides CSRF defense.\nSupports github.com and GitHub Enterprise Server (per-tenant `host`).\n"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT access token for authentication (Bearer token)"},"TenantId":{"type":"apiKey","in":"header","name":"X-Tenant-Id","description":"Tenant identifier for multi-tenancy isolation (optional - defaults to first org from JWT)"},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Scoped API key for machine-to-machine authentication.\nKeys are created per-entity and carry explicit scopes (e.g., `decisions:write`, `status:write`).\n"}},"schemas":{"DashboardMetrics":{"type":"object","required":["performance","bundleStatus","systems","decisionStats","decisionVolumeSeries","policyStats","fleetMetrics","meta"],"properties":{"performance":{"$ref":"#/components/schemas/PerformanceMetrics"},"bundleStatus":{"$ref":"#/components/schemas/BundleStatus"},"systems":{"type":"array","items":{"$ref":"#/components/schemas/SystemHealth"}},"decisionStats":{"$ref":"#/components/schemas/DecisionStats"},"decisionVolumeSeries":{"type":"array","description":"Time-bucketed decision counts for volume chart","items":{"$ref":"#/components/schemas/DecisionVolumePoint"}},"policyStats":{"$ref":"#/components/schemas/PolicyStats"},"fleetMetrics":{"$ref":"#/components/schemas/FleetMetrics"},"meta":{"$ref":"#/components/schemas/DashboardMeta"}}},"PerformanceMetrics":{"type":"object","required":["avgBundleLoadDurationMs","bundleLoadsTotal","bundleLoadsFailed","lastBundleActivation"],"properties":{"avgBundleLoadDurationMs":{"type":"number","nullable":true,"description":"Average bundle load duration in milliseconds"},"bundleLoadsTotal":{"type":"integer","minimum":0,"description":"Total number of bundle deployments (last 24h)"},"bundleLoadsFailed":{"type":"integer","minimum":0,"description":"Number of failed bundle deployments (last 24h)"},"lastBundleActivation":{"type":"string","format":"date-time","nullable":true,"description":"ISO timestamp of last successful bundle activation"}}},"BundleStatus":{"type":"object","required":["successfulLoads","failedLoads","successRate"],"properties":{"successfulLoads":{"type":"integer","minimum":0,"description":"Number of successful deployments (last 24h)"},"failedLoads":{"type":"integer","minimum":0,"description":"Number of failed deployments (last 24h)"},"successRate":{"type":"number","minimum":0,"maximum":100,"description":"Success rate as percentage (0-100)"}}},"SystemHealth":{"type":"object","required":["entityId","entityName","isHealthy","lastDeploymentStatus","lastDeploymentAt"],"properties":{"entityId":{"type":"string","format":"uuid","description":"Entity UUID"},"entityName":{"type":"string","description":"Entity display name"},"isHealthy":{"type":"boolean","description":"Whether the system is healthy (lastDeploymentStatus === 'success')"},"lastDeploymentStatus":{"type":"string","nullable":true,"enum":["success","failed","timeout","pending","in_progress"],"description":"Status of the most recent deployment"},"lastDeploymentAt":{"type":"string","format":"date-time","nullable":true,"description":"ISO timestamp of the most recent deployment"}}},"DecisionStats":{"type":"object","required":["totalCount","avgDurationMs"],"properties":{"totalCount":{"type":"integer","minimum":0,"description":"Total number of decisions in the selected time range"},"avgDurationMs":{"type":"number","nullable":true,"description":"Average decision evaluation duration in milliseconds"},"p99DurationMs":{"type":"number","nullable":true,"description":"P99 decision evaluation duration in milliseconds"},"peakDurationMs":{"type":"number","nullable":true,"description":"Peak (max) decision evaluation duration in milliseconds"},"durationSeries":{"type":"array","description":"Duration sparkline data points (avg duration per time bucket, in ms). Null items = no data for that bucket.","items":{"type":"number","nullable":true}}}},"DecisionVolumePoint":{"type":"object","required":["timestamp","count"],"properties":{"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 timestamp for the start of the time bucket"},"count":{"type":"integer","minimum":0,"description":"Number of decisions in this time bucket"}}},"PolicyStats":{"type":"object","required":["totalSystems","activeSystems","errorCount","evaluatingCount","draftCount","disabledCount","addedThisMonth","trend7d"],"properties":{"totalSystems":{"type":"integer","minimum":0,"description":"Number of system entities with a policy_source config"},"activeSystems":{"type":"integer","minimum":0,"description":"Number of system entities with a successful deployment in the selected time range"},"errorCount":{"type":"integer","minimum":0,"description":"Number of policies in error state (latest deployment failed/timeout)"},"evaluatingCount":{"type":"integer","minimum":0,"description":"Number of policies currently evaluating (latest deployment in_progress/pending)"},"draftCount":{"type":"integer","minimum":0,"description":"Number of draft policies (has policy_source but no deployments)"},"disabledCount":{"type":"integer","minimum":0,"description":"Number of disabled policies"},"addedThisMonth":{"type":"integer","minimum":0,"description":"Number of policies added this calendar month"},"trend7d":{"type":"array","description":"7-day trend sparkline data (daily active counts, oldest first)","items":{"type":"integer"}}}},"FleetMetrics":{"type":"object","required":["avgLatencyMs","uptimePercent","throughputPerSec"],"properties":{"avgLatencyMs":{"type":"number","nullable":true,"description":"Average PDP evaluation latency in milliseconds"},"uptimePercent":{"type":"number","nullable":true,"description":"Fleet uptime percentage (percent of current PDP instances that are healthy)"},"throughputPerSec":{"type":"number","nullable":true,"description":"Decision throughput in decisions per second"},"latencySeries":{"type":"array","description":"Latency sparkline data points (avg duration per bucket, in ms). Null items = no data for that bucket.","items":{"type":"number","nullable":true}},"throughputSeries":{"type":"array","description":"Throughput sparkline data points (decisions/sec per bucket). Null items = no data for that bucket.","items":{"type":"number","nullable":true}},"p99LatencyMs":{"type":"number","nullable":true,"description":"P99 latency in ms"},"peakLatencyMs":{"type":"number","nullable":true,"description":"Peak latency in ms"},"totalDowntimeMinutes":{"type":"number","nullable":true,"description":"Total downtime in minutes (30-day rolling window)"},"peakThroughput":{"type":"number","nullable":true,"description":"Peak throughput in decisions/sec"},"minThroughput":{"type":"number","nullable":true,"description":"Minimum throughput in decisions/sec"}}},"DashboardMeta":{"type":"object","required":["dataAsOf"],"properties":{"dataAsOf":{"type":"string","format":"date-time","description":"ISO timestamp of when the materialized view was last refreshed"}}},"Error":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","description":"i18n error token","example":"user_not_found"},"message":{"type":"string","description":"Human-readable error message (optional)"}}},"ValidationErrorDetail":{"type":"object","required":["code","message","field"],"properties":{"code":{"type":"string","description":"Error code (e.g., invalid, required, minLength)","example":"invalid"},"message":{"type":"string","description":"Human-readable error message","example":"Invalid email format"},"field":{"type":"string","description":"Dot-separated field path (e.g., user.email, limit)","example":"user.email"}}},"ValidationError":{"type":"object","required":["success","error","message","errors"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","description":"i18n error token","example":"validation_error"},"message":{"type":"string","description":"Human-readable error message","example":"Validation failed"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ValidationErrorDetail"},"description":"Array of field-level validation errors"}}},"Entity":{"type":"object","required":["id","tenantId","name","entityType","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Unique entity identifier","example":"550e8400-e29b-41d4-a716-446655440000"},"tenantId":{"type":"string","format":"uuid","description":"Tenant identifier"},"name":{"type":"string","maxLength":255,"description":"Entity name","example":"Acme Corp"},"entityType":{"type":"string","maxLength":100,"description":"Entity type","example":"org","enum":["org","system","user","api"]},"description":{"type":"string","nullable":true,"description":"Entity description","example":"Main organization for Acme Corp"},"metadata":{"type":"object","nullable":true,"description":"Flexible entity-specific data"},"externalId":{"type":"string","maxLength":255,"nullable":true,"description":"External system ID (e.g., Okta org ID)","example":"org-12345"},"externalSystem":{"type":"string","maxLength":50,"nullable":true,"description":"External system name (okta, azure_ad, google, manual)","example":"okta"},"syncEnabled":{"type":"boolean","default":false,"description":"Whether sync with external system is enabled","example":false},"lastSyncedAt":{"type":"string","format":"date-time","nullable":true,"description":"Last sync timestamp"},"createdAt":{"type":"string","format":"date-time","description":"Entity creation timestamp","example":"2025-01-15T09:30:00.000Z"},"updatedAt":{"type":"string","format":"date-time","description":"Last update timestamp","example":"2025-01-15T09:30:00.000Z"}}},"EntityCreate":{"type":"object","required":["name","entityType"],"properties":{"name":{"type":"string","maxLength":255,"description":"Entity name"},"entityType":{"type":"string","maxLength":100,"description":"Entity type (org or system)","enum":["org","system"]},"description":{"type":"string","description":"Entity description"},"metadata":{"type":"object","description":"Flexible entity-specific data"},"externalId":{"type":"string","maxLength":255,"description":"External system ID"},"externalSystem":{"type":"string","maxLength":50,"description":"External system name"}}},"EntityUpdate":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"Entity name"},"entityType":{"type":"string","maxLength":100,"description":"Entity type"},"description":{"type":"string","nullable":true,"description":"Entity description"},"metadata":{"type":"object","description":"Flexible entity-specific data"},"externalId":{"type":"string","maxLength":255,"nullable":true,"description":"External system ID"},"externalSystem":{"type":"string","maxLength":50,"nullable":true,"description":"External system name"}}},"EntityCreateWithRelation":{"type":"object","required":["entity","relation"],"properties":{"entity":{"$ref":"#/components/schemas/EntityCreate"},"relation":{"type":"object","required":["targetEntityId","relationType","direction"],"properties":{"targetEntityId":{"type":"string","format":"uuid","description":"The entity to relate to (the new entity will be the other side)"},"relationType":{"type":"string","description":"Relation type (e.g., 'parent_of', 'member_of', 'joint_venture')"},"direction":{"type":"string","enum":["from_new","to_new"],"default":"to_new","description":"Direction of the relation:\n- from_new: new entity -> target (new entity is parent/source)\n- to_new: target -> new entity (new entity is child/target)\n"},"weight":{"type":"number","description":"Optional weight for ordering relations"},"metadata":{"type":"object","description":"Optional metadata for the relation"}}}}},"EntityWithRelations":{"allOf":[{"$ref":"#/components/schemas/Entity"},{"type":"object","required":["parents","children"],"properties":{"parents":{"type":"array","items":{"$ref":"#/components/schemas/Entity"},"description":"Parent entities"},"children":{"type":"array","items":{"$ref":"#/components/schemas/Entity"},"description":"Child entities"}}}]},"EntityTreeNode":{"allOf":[{"$ref":"#/components/schemas/Entity"},{"type":"object","required":["children"],"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/EntityTreeNode"},"description":"Child entities (recursive)"}}}]},"EntityRelation":{"type":"object","required":["id","tenantId","fromEntityId","toEntityId","relationType","weight","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Relation identifier"},"tenantId":{"type":"string","format":"uuid","description":"Tenant identifier"},"fromEntityId":{"type":"string","format":"uuid","description":"Parent entity ID"},"toEntityId":{"type":"string","format":"uuid","description":"Child entity ID"},"relationType":{"type":"string","default":"parent_of","description":"Type of relation"},"weight":{"type":"integer","default":1,"description":"Weight for config inheritance priority"},"metadata":{"type":"object","nullable":true,"description":"Relation-specific metadata"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"RelationCreate":{"type":"object","required":["toEntityId"],"properties":{"fromEntityId":{"type":"string","format":"uuid","description":"Parent entity ID (optional, can use path parameter)"},"toEntityId":{"type":"string","format":"uuid","description":"Child entity ID"},"relationType":{"type":"string","default":"parent_of","description":"Type of relation"},"weight":{"type":"integer","default":1,"description":"Weight for config inheritance priority"},"metadata":{"type":"object","description":"Relation-specific metadata"}}},"EntityResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/Entity"}}},"EntityWithRelationResponse":{"type":"object","description":"Response for creating an entity with a relation (includes the created relation)","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"allOf":[{"$ref":"#/components/schemas/Entity"},{"type":"object","required":["relation"],"properties":{"relation":{"$ref":"#/components/schemas/EntityRelation"}}}]}}},"EntityUser":{"type":"object","required":["user","role","joinedAt","status"],"properties":{"user":{"$ref":"#/components/schemas/Entity"},"role":{"type":"string","description":"User role in the entity"},"joinedAt":{"type":"string","format":"date-time","description":"When the user joined the entity"},"status":{"type":"string","enum":["active","inactive"],"description":"Membership status"}}},"EntityUserListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/EntityUser"}}}},"EntityUserResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/EntityUser"}}},"AddUserToEntityRequest":{"type":"object","required":["userId","role"],"properties":{"userId":{"type":"string","format":"uuid","description":"ID of the user entity to add"},"role":{"type":"string","minLength":1,"maxLength":50,"description":"Role to assign to the user"}}},"UpdateEntityUserRoleRequest":{"type":"object","required":["role"],"properties":{"role":{"type":"string","minLength":1,"maxLength":50,"description":"New role for the user"}}},"EntityWithRelationsResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/EntityWithRelations"}}},"EntityListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Entity"}}}},"EntityTreeResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/EntityTreeNode"}}}},"RelationResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/EntityRelation"}}},"RelationListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/EntityRelation"}}}},"PolicySourceConfigGit":{"type":"object","required":["storage_type","policy_engine","repository_service","repository_url"],"properties":{"storage_type":{"type":"string","enum":["git"]},"policy_engine":{"type":"string","enum":["opa","arbi","ibar"],"description":"Policy Decision Point (PDP) type:\n- opa: Open Policy Agent (Rego PDP)\n- arbi: Cedar PDP\n- ibar: Zanzibar PDP\n"},"repository_service":{"type":"string","enum":["github","gitlab","bitbucket","custom"]},"repository_url":{"type":"string","format":"uri","description":"Full git repository URL"},"branch":{"type":"string","default":"main","description":"Git branch"},"policy_path":{"type":"string","description":"Path within repository (optional, supports inheritance)"},"policy_path_mode":{"type":"string","enum":["append","override"],"default":"override","description":"How to merge paths during inheritance"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator), not actual credentials"},"credentials_type":{"type":"string","enum":["aws_s3","aws_iam_role","generic"],"description":"Type of credential stored at credentials_ref"}}},"PolicySourceConfigS3":{"type":"object","required":["storage_type","policy_engine","bucket","region"],"properties":{"storage_type":{"type":"string","enum":["s3"]},"policy_engine":{"type":"string","enum":["opa","arbi","ibar"],"description":"Policy Decision Point (PDP) type:\n- opa: Open Policy Agent (Rego PDP)\n- arbi: Cedar PDP\n- ibar: Zanzibar PDP\n"},"bucket":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9][a-z0-9.-]*[a-z0-9]$","description":"S3 bucket name"},"region":{"type":"string","minLength":1,"description":"AWS region"},"policy_path":{"type":"string","description":"Path within S3 bucket"},"policy_path_mode":{"type":"string","enum":["append","override"],"default":"override"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator)"},"credentials_type":{"type":"string","enum":["aws_s3","aws_iam_role","generic"],"description":"Type of credential stored at credentials_ref"}}},"PolicySourceConfigGCS":{"type":"object","required":["storage_type","policy_engine","bucket","project_id"],"properties":{"storage_type":{"type":"string","enum":["gcs"]},"policy_engine":{"type":"string","enum":["opa","arbi","ibar"],"description":"Policy Decision Point (PDP) type:\n- opa: Open Policy Agent (Rego PDP)\n- arbi: Cedar PDP\n- ibar: Zanzibar PDP\n"},"bucket":{"type":"string","minLength":3,"pattern":"^[a-z0-9][a-z0-9._-]*[a-z0-9]$","description":"GCS bucket name (max 63 chars, or 222 if dot-separated)"},"project_id":{"type":"string","minLength":1,"description":"GCP project ID"},"policy_path":{"type":"string","description":"Path prefix within GCS bucket"},"policy_path_mode":{"type":"string","enum":["append","override"],"default":"override"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator) for GCS service account key"},"credentials_type":{"type":"string","enum":["gcs_service_account"],"description":"Type of credential stored at credentials_ref"}}},"PolicySourceConfigAzure":{"type":"object","required":["storage_type","policy_engine","container_name","storage_account_name","credentials_ref"],"properties":{"storage_type":{"type":"string","enum":["azure"]},"policy_engine":{"type":"string","enum":["opa","arbi","ibar"],"description":"Policy Decision Point (PDP) type:\n- opa: Open Policy Agent (Rego PDP)\n- arbi: Cedar PDP\n- ibar: Zanzibar PDP\n"},"container_name":{"type":"string","minLength":3,"maxLength":63,"pattern":"^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])$","description":"Azure Blob Storage container name (lowercase alphanumeric and hyphens; no consecutive hyphens)"},"storage_account_name":{"type":"string","minLength":3,"maxLength":24,"pattern":"^[a-z0-9]+$","description":"Azure storage account name (lowercase alphanumeric only)"},"policy_path":{"type":"string","description":"Blob path prefix within Azure container"},"policy_path_mode":{"type":"string","enum":["append","override"],"default":"override"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator) for Azure storage account key"},"credentials_type":{"type":"string","enum":["azure_blob"],"default":"azure_blob","description":"Type of credential stored at credentials_ref"}}},"PolicySourceConfigFilesystem":{"type":"object","required":["storage_type","policy_engine","filesystem_path"],"properties":{"storage_type":{"type":"string","enum":["filesystem"]},"policy_engine":{"type":"string","enum":["opa","arbi","ibar"],"description":"Policy Decision Point (PDP) type:\n- opa: Open Policy Agent (Rego PDP)\n- arbi: Cedar PDP\n- ibar: Zanzibar PDP\n"},"filesystem_path":{"type":"string","minLength":1,"description":"Local filesystem path (development only)"},"policy_path_mode":{"type":"string","enum":["append","override"],"default":"override"}}},"PolicySourceConfig":{"oneOf":[{"$ref":"#/components/schemas/PolicySourceConfigGit"},{"$ref":"#/components/schemas/PolicySourceConfigS3"},{"$ref":"#/components/schemas/PolicySourceConfigGCS"},{"$ref":"#/components/schemas/PolicySourceConfigAzure"},{"$ref":"#/components/schemas/PolicySourceConfigFilesystem"}]},"BundleDestinationConfigS3":{"type":"object","required":["storage_type","bucket","region"],"properties":{"storage_type":{"type":"string","enum":["s3"]},"bucket":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9][a-z0-9.-]*[a-z0-9]$"},"region":{"type":"string","minLength":1},"bundle_path":{"type":"string","description":"Path within S3 bucket for bundles"},"bundle_path_mode":{"type":"string","enum":["append","override"],"default":"override","description":"Path composition mode (append joins with parent, override replaces)"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator)"},"credentials_type":{"type":"string","enum":["aws_s3","aws_iam_role","generic"],"description":"Type of credential stored at credentials_ref"}}},"BundleDestinationConfigGCS":{"type":"object","required":["storage_type","bucket","project_id"],"properties":{"storage_type":{"type":"string","enum":["gcs"]},"bucket":{"type":"string","minLength":3,"pattern":"^[a-z0-9][a-z0-9._-]*[a-z0-9]$","description":"GCS bucket name (max 63 chars, or 222 if dot-separated)"},"project_id":{"type":"string","minLength":1,"description":"GCP project ID"},"bundle_path":{"type":"string","description":"Object path for the bundle file (e.g., policies/bundle.tar.gz). Must not end with /"},"bundle_path_mode":{"type":"string","enum":["append","override"],"default":"override","description":"Path composition mode (append joins with parent, override replaces)"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator) for GCS service account key"},"credentials_type":{"type":"string","enum":["gcs_service_account"],"description":"Type of credential stored at credentials_ref"}}},"BundleDestinationConfigAzure":{"type":"object","required":["storage_type","container_name","storage_account_name","credentials_ref"],"properties":{"storage_type":{"type":"string","enum":["azure"]},"container_name":{"type":"string","minLength":3,"maxLength":63,"pattern":"^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])$","description":"Azure Blob Storage container name (lowercase alphanumeric and hyphens; no consecutive hyphens)"},"storage_account_name":{"type":"string","minLength":3,"maxLength":24,"pattern":"^[a-z0-9]+$","description":"Azure storage account name (lowercase alphanumeric only)"},"bundle_path":{"type":"string","pattern":"^.*[^/]$","description":"Blob path for the bundle file (e.g., policies/bundle.tar.gz). Must not end with /"},"bundle_path_mode":{"type":"string","enum":["append","override"],"default":"override","description":"Path composition mode (append joins with parent, override replaces)"},"credentials_ref":{"type":"string","description":"Secret reference (ARN/locator) for Azure storage account key"},"credentials_type":{"type":"string","enum":["azure_blob"],"default":"azure_blob","description":"Type of credential stored at credentials_ref"}}},"BundleDestinationConfig":{"oneOf":[{"$ref":"#/components/schemas/BundleDestinationConfigS3"},{"$ref":"#/components/schemas/BundleDestinationConfigGCS"},{"$ref":"#/components/schemas/BundleDestinationConfigAzure"}]},"EnvironmentBundleDestinationOverride":{"type":"object","additionalProperties":false,"description":"Environment-level partial override for bundle_destination (EA-601). Only the fields that legitimately vary per environment may appear here; storage backend identity (storage_type, bucket, region, container_name, storage_account_name, project_id, filesystem_path) is inherited from the entity's effective BundleDestinationConfig at deploy time. Server-side validation rejects requests that include any of those inherited-only fields. This schema is BREAKING vs. the pre-EA-601 EnvironmentConfig.environments[].bundle_destination shape, which referenced the full BundleDestinationConfig oneOf. External consumers reading env_config directly should switch to this shape; legacy persisted rows are sanitized to this shape on read.","properties":{"bundle_path":{"type":"string","pattern":".*[^/]$","description":"Object key/path for the bundle within the inherited bucket/container. Cannot end with \"/\" — must address a single bundle file, not a folder. Mirrors the backend Zod refine in EnvironmentBundleDestinationOverrideSchema."},"bundle_path_mode":{"type":"string","enum":["append","override"],"description":"Override bundle_path_mode for this environment"},"credentials_ref":{"type":"string","description":"Override credentials for this environment (e.g., prod uses a different IAM role)"},"credentials_type":{"type":"string","enum":["aws_s3","aws_iam_role","gcs_service_account","azure_blob","generic"],"description":"Type of credential stored at credentials_ref"}}},"EnvironmentConfig":{"type":"object","required":["environments","default_environment"],"properties":{"environments":{"type":"object","description":"Map of environment name to environment definition","additionalProperties":{"type":"object","required":["policy_source","auto_deploy"],"properties":{"policy_source":{"type":"object","description":"Policy source overrides for this environment. Applied on top of the entity's effective PolicySourceConfig. At least one of branch or policy_path must be provided (they discriminate environments); server-side validation enforces this constraint. All other fields are optional and inherit from the entity/ancestor config.","properties":{"branch":{"type":"string","minLength":1,"description":"Git branch name (e.g., \"develop\", \"main\"). Required for git sources; omit for cloud bucket sources (S3, GCS, Azure)."},"policy_path":{"type":"string","maxLength":500,"pattern":"^(?![/\\\\])(?!.*\\.\\.)(?![A-Za-z]:[/\\\\]).*$","description":"Policy path prefix for this environment. For git path model: folder within the repo (e.g., \"dev/\", \"stage/\", \"prod/\"). For cloud bucket sources: object key prefix (e.g., \"dev/\", \"prod/policies/\"). Must be a relative path (no leading / or \\, no \"..\" traversal). Required for cloud bucket sources."},"policy_path_mode":{"type":"string","enum":["append","override"],"description":"Override policy_path_mode for this environment"},"credentials_ref":{"type":"string","description":"Override credentials for this environment (e.g., prod uses a different PAT)"},"credentials_type":{"type":"string","enum":["aws_s3","aws_iam_role","gcs_service_account","azure_blob","generic"],"description":"Type of credential stored"}}},"auto_deploy":{"type":"boolean","description":"Whether webhook pushes to this branch trigger automatic deployment"},"bundle_destination":{"$ref":"#/components/schemas/EnvironmentBundleDestinationOverride"},"promotes_to":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$","description":"Name of the target environment for promotions"}}}},"default_environment":{"type":"string","nullable":true,"minLength":1,"maxLength":100,"description":"Default environment for manual deploys (must reference an existing environment)"}}},"EntityConfig":{"type":"object","required":["config_type","config_data"],"properties":{"config_type":{"type":"string","enum":["policy_source","bundle_destination","environment_config"],"description":"Configuration type"},"config_data":{"oneOf":[{"$ref":"#/components/schemas/PolicySourceConfig"},{"$ref":"#/components/schemas/BundleDestinationConfig"},{"$ref":"#/components/schemas/EnvironmentConfig"}],"description":"Configuration data (discriminated by config_type)"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ConfigSource":{"type":"object","required":["entity_id","entity_name","entity_type","depth","config_data"],"properties":{"entity_id":{"type":"string","format":"uuid"},"entity_name":{"type":"string"},"entity_type":{"type":"string"},"depth":{"type":"integer","minimum":0,"description":"Distance from current entity (0 = self, higher = ancestor)"},"config_data":{"oneOf":[{"$ref":"#/components/schemas/PolicySourceConfig"},{"$ref":"#/components/schemas/BundleDestinationConfig"},{"$ref":"#/components/schemas/EnvironmentConfig"}]}}},"ValidationResult":{"type":"object","required":["valid","errors","warnings"],"properties":{"valid":{"type":"boolean","description":"Whether config passes validation"},"errors":{"type":"array","items":{"type":"string"},"description":"Blocking validation errors (i18n tokens)"},"warnings":{"type":"array","items":{"type":"string"},"description":"Non-blocking warnings (i18n tokens)"}}},"GetConfigResponse":{"type":"object","description":"A single entity configuration with its resolved config data","required":["success","entity_id","config_type","config_data"],"properties":{"success":{"type":"boolean","enum":[true]},"entity_id":{"type":"string","format":"uuid","description":"The entity this configuration belongs to"},"config_type":{"type":"string","description":"Configuration type discriminator (e.g., policy_source, bundle_destination, environment_config)"},"config_data":{"description":"The configuration payload, varying by config_type","oneOf":[{"$ref":"#/components/schemas/PolicySourceConfig"},{"$ref":"#/components/schemas/BundleDestinationConfig"},{"$ref":"#/components/schemas/EnvironmentConfig"}]},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when this configuration was created"},"updated_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of the last configuration update"}}},"ListConfigsResponse":{"type":"object","description":"All configurations attached to a specific entity","required":["success","entity_id","configs"],"properties":{"success":{"type":"boolean","enum":[true]},"entity_id":{"type":"string","format":"uuid","description":"The entity whose configurations are listed"},"configs":{"type":"array","description":"Array of configuration objects for this entity","items":{"$ref":"#/components/schemas/EntityConfig"}}}},"EffectiveConfigResponse":{"type":"object","description":"The resolved effective configuration for an entity, computed by walking the entity hierarchy","required":["success","entity_id","config_type","effective_config","config_sources","validation"],"properties":{"success":{"type":"boolean","enum":[true]},"entity_id":{"type":"string","format":"uuid","description":"The entity for which the effective config was resolved"},"config_type":{"type":"string","description":"Configuration type discriminator (e.g., policy_source, bundle_destination, environment_config)"},"effective_config":{"oneOf":[{"$ref":"#/components/schemas/PolicySourceConfig"},{"$ref":"#/components/schemas/BundleDestinationConfig"},{"$ref":"#/components/schemas/EnvironmentConfig"}]},"config_sources":{"type":"array","items":{"$ref":"#/components/schemas/ConfigSource"}},"validation":{"$ref":"#/components/schemas/ValidationResult"}}},"VerifyConfigRequest":{"type":"object","required":["config_type","config_data"],"properties":{"config_type":{"type":"string","enum":["policy_source","bundle_destination","environment_config"]},"config_data":{"oneOf":[{"$ref":"#/components/schemas/PolicySourceConfig"},{"$ref":"#/components/schemas/BundleDestinationConfig"},{"$ref":"#/components/schemas/EnvironmentConfig"}]}}},"VerifyConfigResponse":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean"},"message":{"type":"string","description":"Verification result message (i18n token)"},"error":{"type":"string","description":"Error code if verification failed (i18n token)"},"details":{"type":"object","description":"Additional verification details"}}},"StoreSecretRequest":{"type":"object","required":["entityType","entityId","secretName","secretValue"],"properties":{"entityType":{"type":"string","enum":["org","system"],"description":"Entity type the secret belongs to"},"entityId":{"type":"string","format":"uuid","description":"Entity ID the secret belongs to"},"secretName":{"type":"string","minLength":1,"maxLength":255,"description":"Name/key for the secret"},"secretValue":{"type":"string","minLength":1,"description":"The actual secret value to store"},"credentialType":{"type":"string","enum":["aws_s3","aws_iam_role","generic","gcs_service_account","azure_blob"],"default":"generic","description":"Type of credential being stored (defaults to generic)"}}},"SecretMetadata":{"type":"object","required":["id","tenantId","secretName","entityType","entityId","provider","locator","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Secret identifier"},"tenantId":{"type":"string","format":"uuid","description":"Tenant identifier"},"secretName":{"type":"string","description":"Secret name/key"},"entityType":{"type":"string","enum":["org","system"],"description":"Entity type"},"entityId":{"type":"string","format":"uuid","description":"Entity ID"},"provider":{"type":"string","description":"Secret provider (e.g., aws_secrets_manager)"},"locator":{"type":"string","description":"Provider-specific locator (e.g., ARN)"},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp"},"updatedAt":{"type":"string","format":"date-time","description":"Last update timestamp"}}},"StoreSecretResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","locator","provider"],"properties":{"id":{"type":"string","format":"uuid","description":"Secret identifier"},"locator":{"type":"string","description":"Provider-specific locator (e.g., ARN)"},"provider":{"type":"string","description":"Secret provider (e.g., aws_secrets_manager)"}}}}},"SecretMetadataResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/SecretMetadata"}}},"RotateSecretRequest":{"type":"object","required":["secretValue"],"properties":{"secretValue":{"type":"string","minLength":1,"description":"The new secret value"}}},"OPADecisionLog":{"type":"object","additionalProperties":true,"required":["labels","decision_id","timestamp","path"],"properties":{"decision_id":{"type":"string","description":"Unique identifier for the decision from OPA"},"timestamp":{"type":"string","description":"ISO 8601 timestamp (OPA may use nanosecond precision)"},"path":{"type":"string","description":"Policy path evaluated"},"input":{"type":"object","additionalProperties":true,"description":"Input document for the decision"},"result":{"description":"Decision result (can be boolean, object, or any value)","anyOf":[{"type":"boolean"},{"type":"object","additionalProperties":true},{"type":"string"},{"type":"number"},{"type":"array","items":{}}]},"labels":{"type":"object","properties":{"id":{"type":"string","description":"OPA instance identifier"},"version":{"type":"string","description":"OPA version"}},"additionalProperties":true,"description":"OPA instance labels"},"bundles":{"type":"object","additionalProperties":{"type":"object","required":["revision"],"properties":{"revision":{"type":"string"}}},"description":"Bundle revisions by bundle name"},"metrics":{"type":"object","properties":{"timer_rego_query_eval_ns":{"type":"integer","description":"Query evaluation time in nanoseconds"}},"additionalProperties":true,"description":"Performance metrics"},"req_id":{"type":"integer","description":"Request identifier"},"nd_builtin_cache":{"type":"object","additionalProperties":true,"description":"Non-deterministic builtin cache"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Error code"},"message":{"type":"string","description":"Error message"},"details":{"description":"Additional error details"}},"description":"Error information (if policy evaluation failed)"}}},"IngestDecisionLogsRequest":{"description":"Accepts two formats:\n1. OPA native format: array of logs directly [...] (gzip compressed)\n2. Wrapped format: { logs: [...] } (for manual testing)\n\nNote: Basic type validation (array or object) is performed at the gateway level.\nDetailed OPA decision log schema validation is performed server-side.\n\nSee: https://www.openpolicyagent.org/docs/management-decision-logs\n","anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/OPADecisionLog"},"minItems":1,"maxItems":10000},{"type":"object","required":["logs"],"properties":{"logs":{"type":"array","items":{"$ref":"#/components/schemas/OPADecisionLog"},"minItems":1,"maxItems":10000,"description":"Array of OPA decision logs"}}}]},"IngestDecisionLogsResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["accepted","failed"],"properties":{"accepted":{"type":"integer","minimum":0,"description":"Number of logs successfully ingested"},"failed":{"type":"integer","minimum":0,"description":"Number of logs that failed to ingest"}}}}},"DecisionLogEntry":{"type":"object","required":["id","tenant_id","decision_id","timestamp","path","decision_outcome","received_at","partition_date"],"properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier (generated by EnforceAuth)"},"tenant_id":{"type":"string","format":"uuid","description":"Tenant identifier for multi-tenancy scoping"},"decision_id":{"type":"string","description":"Original decision ID from OPA","example":"880e8400-e29b-41d4-a716-446655440000"},"timestamp":{"type":"string","format":"date-time","description":"Decision timestamp from OPA","example":"2025-01-15T09:30:00.000Z"},"path":{"type":"string","description":"Policy path evaluated","example":"authz/allow"},"input":{"type":"object","additionalProperties":true,"description":"Input document for the decision"},"result":{"description":"Decision result (can be boolean, object, or any value)","anyOf":[{"type":"boolean"},{"type":"object","additionalProperties":true},{"type":"string"},{"type":"number"},{"type":"array","items":{}}]},"decision_outcome":{"type":"string","enum":["allow","deny","error","partial"],"description":"Derived decision outcome for analytics","example":"allow"},"system_id":{"type":"string","description":"OPA instance identifier","example":"opa-agent-prod-01"},"system_version":{"type":"string","description":"OPA version","example":"0.68.0"},"labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Full OPA labels object (preserved for custom labels)"},"bundle_revision":{"type":"string","description":"Primary bundle revision","example":"2025-01-15T09:25:00Z"},"bundles":{"type":"object","additionalProperties":{"type":"object","required":["revision"],"properties":{"revision":{"type":"string"}}},"description":"All bundle revisions"},"evaluation_duration_ns":{"type":"integer","description":"Query evaluation time in nanoseconds","example":125000},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"description":"Error information (if evaluation failed)"},"received_at":{"type":"string","format":"date-time","description":"When EnforceAuth received the log","example":"2025-01-15T09:30:01.000Z"},"partition_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Date partition (YYYY-MM-DD) for Hive partitioning","example":"2025-01-15"}}},"DecisionLogSummary":{"type":"object","description":"Lightweight decision log entry for list responses.\nExcludes heavy JSON blobs (result, bundles, labels, error) that are only\nreturned in detail responses. Use GET /v1/logs/{id} for full data.\n","required":["id","tenant_id","decision_id","timestamp","path","decision_outcome","received_at"],"properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier (generated by EnforceAuth)"},"tenant_id":{"type":"string","format":"uuid","description":"Tenant identifier for multi-tenancy scoping"},"entity_id":{"type":"string","description":"EnforceAuth entity association (from API key auth)"},"decision_id":{"type":"string","description":"Original decision ID from OPA"},"timestamp":{"type":"string","format":"date-time","description":"Decision timestamp from OPA"},"path":{"type":"string","description":"Policy path evaluated"},"input":{"type":"object","additionalProperties":true,"description":"Input document for the decision"},"decision_outcome":{"type":"string","enum":["allow","deny","error","partial"],"description":"Derived decision outcome for analytics"},"system_id":{"type":"string","description":"OPA instance identifier"},"system_version":{"type":"string","description":"OPA version"},"bundle_revision":{"type":"string","description":"Primary bundle revision"},"evaluation_duration_ns":{"type":"integer","description":"Query evaluation time in nanoseconds"},"received_at":{"type":"string","format":"date-time","description":"When EnforceAuth received the log"}}},"DecisionLogListResponse":{"type":"object","required":["success","data","pagination"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DecisionLogSummary"}},"pagination":{"type":"object","required":["has_more"],"properties":{"cursor":{"type":"string","description":"Cursor for fetching next page (base64 encoded)"},"has_more":{"type":"boolean","description":"Whether more results are available"}}},"total_count":{"type":"integer","minimum":0,"description":"Total number of matching logs across all pages. Only present\nwhen the backend was able to compute it (depends on filter\nselectivity and provider capability). Absent for queries where\ncounting would be too expensive.\n"},"outcome_counts":{"type":"object","description":"Per-outcome counts for the full filtered result set (before\npagination). Only present when the backend was able to compute\nit.\n","required":["allow","deny","error","partial"],"properties":{"allow":{"type":"integer","minimum":0},"deny":{"type":"integer","minimum":0},"error":{"type":"integer","minimum":0},"partial":{"type":"integer","minimum":0}}}}},"DecisionLogResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/DecisionLogEntry"}}},"LogReplayRequest":{"type":"object","description":"Request for bulk log replay / impact analysis.\nTest draft policies or data patches against historical decisions.\n","properties":{"entity_id":{"type":"string","minLength":20,"maxLength":36,"pattern":"^[a-zA-Z0-9_-]+$","description":"Entity ID for bundle lookup. Determines which entity's bundle_destination\nconfig to use for replay. Required for async log replay.\n\nAccepts UUID, ULID, or other flexible ID formats (20-36 chars).\nFuture enhancement: Will be automatically inferred from system_id -> entity\nmapping once the systems registry is implemented.\n","example":"00000000-0000-0000-0000-000000000001"},"policies":{"type":"object","additionalProperties":{"type":"string"},"description":"Draft Rego policies to test. Keys are file paths (e.g., \"authz/rules.rego\"),\nvalues are Rego source code. Evaluated using direct Rego evaluation (Go).\n","example":{"authz/rules.rego":"package authz\ndefault allow = false\nallow { input.user == \"admin\" }\n"}},"data_patches":{"type":"array","description":"JSON Patch operations (RFC 6902) to apply to the data namespace.\nAllows testing changes to reference data without modifying policies.\n","items":{"$ref":"#/components/schemas/JsonPatchOperation"}},"decision_patches":{"type":"array","description":"JSON Patch operations (RFC 6902) to apply to decision inputs.\nAllows modifying inputs before replay for what-if scenarios.\n","items":{"$ref":"#/components/schemas/JsonPatchOperation"}},"scope":{"$ref":"#/components/schemas/LogReplayScope"},"duration":{"type":"string","pattern":"^\\d+[smh]$","default":"60s","description":"Maximum duration for the replay operation (e.g., \"30s\", \"5m\").\nPrevents runaway replays.\n"},"max_samples":{"type":"integer","minimum":0,"maximum":1000,"description":"Maximum number of sample results to include in response (0 = stats only)"},"max_decisions":{"type":"integer","minimum":1,"maximum":100000,"default":10000,"description":"Maximum number of decisions to evaluate. Prevents memory issues\nwhen processing large decision sets.\n"},"deterministic_policies":{"type":"boolean","default":true,"description":"If true, cache evaluation results for identical inputs.\nImproves performance for policies without side effects.\n"},"reason":{"type":"string","maxLength":1000,"description":"Reason for replay (for audit logging)"}}},"LogReplayScope":{"type":"object","description":"Filters for selecting which decisions to replay","properties":{"system_ids":{"type":"array","items":{"type":"string"},"description":"Filter to specific OPA system IDs"},"timestamp_start":{"type":"string","format":"date-time","description":"Start of time range filter (inclusive)"},"timestamp_end":{"type":"string","format":"date-time","description":"End of time range filter (inclusive)"},"path_filters":{"type":"array","items":{"type":"string"},"description":"Filter to specific policy paths"},"decision_outcome":{"type":"string","enum":["allow","deny","error","partial"],"description":"Filter to decisions with a specific outcome"}}},"JsonPatchOperation":{"type":"object","description":"JSON Patch operation (RFC 6902)","required":["op","path"],"properties":{"op":{"type":"string","enum":["add","remove","replace","move","copy","test"],"description":"Patch operation type"},"path":{"type":"string","description":"JSON Pointer to the target location"},"value":{"description":"Value for add/replace/test operations"},"from":{"type":"string","description":"Source location for move/copy operations"}}},"LogReplayStats":{"type":"object","description":"Aggregate statistics from the replay","required":["entries_evaluated","entries_failed","results_changed","allow_to_deny","deny_to_allow"],"properties":{"entries_evaluated":{"type":"integer","description":"Total number of decisions evaluated"},"entries_failed":{"type":"integer","description":"Number of evaluations that failed"},"results_changed":{"type":"integer","description":"Number of decisions with different outcomes"},"allow_to_deny":{"type":"integer","description":"Decisions that changed from allow to deny"},"deny_to_allow":{"type":"integer","description":"Decisions that changed from deny to allow"}}},"LogReplaySample":{"type":"object","description":"Sample result showing original vs replayed decision","required":["decision_id","outcome_changed"],"properties":{"decision_id":{"type":"string","description":"Original decision ID"},"result":{"description":"Original decision result"},"new_result":{"description":"Result after replay with draft policy/patches"},"outcome_changed":{"type":"boolean","description":"Whether the outcome (allow/deny) changed"},"original_outcome":{"type":"string","enum":["allow","deny","error","partial"],"description":"Original decision outcome"},"new_outcome":{"type":"string","enum":["allow","deny","error","partial"],"description":"New decision outcome after replay"}}},"LogReplayResult":{"type":"object","description":"Log replay result data","required":["started","duration","stats","samples"],"properties":{"started":{"type":"string","format":"date-time","description":"When the replay started"},"duration":{"type":"integer","format":"int64","description":"Duration in nanoseconds"},"stats":{"$ref":"#/components/schemas/LogReplayStats"},"samples":{"type":"array","items":{"$ref":"#/components/schemas/LogReplaySample"},"description":"Sample results (up to max_samples)"},"analyzed_batches":{"type":"array","items":{"type":"string"},"description":"System/batch IDs that were analyzed"},"timed_out":{"type":"boolean","description":"Whether the replay was stopped due to duration timeout"}}},"LogReplayResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/LogReplayResult"}}},"LogReplayAsyncJobCreated":{"type":"object","description":"Response when an async log replay job is created","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["job_id","status","created_at","poll_url"],"properties":{"job_id":{"type":"string","format":"uuid","description":"Unique identifier for the async job"},"status":{"type":"string","enum":["pending"],"description":"Initial job status (always pending for newly created jobs)"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when the job was created"},"poll_url":{"type":"string","description":"URL to poll for job status","example":"/v1/logreplay/a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}}}},"LogReplayJobStep":{"type":"object","description":"Progress step within a log replay job","required":["step","status","datapoints"],"properties":{"step":{"type":"string","enum":["fetch","query","eval"],"description":"Step name:\n- fetch: Fetching policy bundle from S3 or compiling from inline policies\n- query: Querying decision logs from Athena for the specified scope\n- eval: Evaluating decisions against policies (direct Rego evaluation)\n"},"status":{"type":"string","enum":["pending","in_progress","completed","failed","skipped"],"description":"Current status of this step"},"started_at":{"type":"string","format":"date-time","description":"Timestamp when the step started"},"completed_at":{"type":"string","format":"date-time","description":"Timestamp when the step completed"},"datapoints":{"type":"object","description":"Step-specific metrics and progress data (always present, may be empty)","additionalProperties":true,"example":{"logs_count":5000}}}},"LogReplayJobStatusResponse":{"type":"object","description":"Response for job status query.\n\nWhen status is 'success', result fields (started, duration, stats, samples)\nare flattened to the top level of the data object. This matches the sync\nAPI response format for consistency.\n","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["job_id","status","created_at","steps"],"properties":{"job_id":{"type":"string","format":"uuid","description":"Unique identifier for the async job"},"status":{"type":"string","enum":["pending","in_progress","success","failed","timeout"],"description":"Current job status"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when the job was created"},"started_at":{"type":"string","format":"date-time","description":"Timestamp when job execution started (present when status is not pending)"},"completed_at":{"type":"string","format":"date-time","description":"Timestamp when job completed (present when status is success, failed, or timeout)"},"duration_ms":{"type":"integer","description":"Duration in milliseconds (present when job is completed)"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/LogReplayJobStep"},"description":"Progress steps for the job (fetch, query, eval)"},"started":{"type":"string","format":"date-time","description":"When evaluation started (present when status is 'success')"},"duration":{"type":"integer","format":"int64","description":"Duration in nanoseconds (present when status is 'success')"},"stats":{"$ref":"#/components/schemas/LogReplayStats","description":"Replay statistics (present when status is 'success')"},"samples":{"type":"array","items":{"$ref":"#/components/schemas/LogReplaySample"},"description":"Sample results up to max_samples (present when status is 'success')"},"timed_out":{"type":"boolean","description":"Whether the replay was stopped due to duration timeout (present when status is 'success' and replay timed out)"},"error":{"type":"object","description":"Error details (present when status is 'failed')","properties":{"code":{"type":"string","description":"Error code"},"message":{"type":"string","description":"Human-readable error message"},"step":{"type":"string","description":"Which step failed"}}}}}}},"Deployment":{"type":"object","required":["run_id","tenant_id","entity_id","trigger_source","status","started_at","timeout_at","created_at","updated_at"],"properties":{"run_id":{"type":"string","minLength":16,"maxLength":16,"description":"Unique deployment run identifier"},"tenant_id":{"type":"string","format":"uuid","description":"Tenant identifier"},"entity_id":{"type":"string","format":"uuid","description":"Entity identifier"},"entity_name":{"type":"string","nullable":true,"description":"Entity name (included in list responses)"},"entity_type":{"type":"string","nullable":true,"description":"Entity type (included in list responses)"},"group_id":{"type":"string","format":"uuid","nullable":true,"description":"Shared group ID for org-level fan-out deployments"},"trigger_source":{"type":"string","description":"How the deployment was triggered (e.g., 'webhook', 'manual')"},"user_id":{"type":"string","format":"uuid","nullable":true,"description":"User who triggered the deployment (manual deploys only)"},"user_name":{"type":"string","nullable":true,"description":"User display name (when enriched)"},"user_email":{"type":"string","nullable":true,"description":"User email (when enriched)"},"repository_url":{"type":"string","nullable":true,"description":"Git repository URL (webhook triggers only)"},"status":{"type":"string","enum":["pending","in_progress","success","failed","timeout"],"description":"Deployment status"},"current_phase":{"type":"string","nullable":true,"description":"Current execution phase (e.g., 'init', 'fetch', 'test', 'build', 'deploy', 'complete')"},"commit_sha":{"type":"string","nullable":true,"description":"Git commit SHA (nullable — manual deploys may have no git metadata)"},"commit_message":{"type":"string","nullable":true,"description":"Git commit message (nullable — manual deploys may have no git metadata)"},"branch":{"type":"string","nullable":true,"description":"Git branch (nullable — manual deploys may have no git metadata)"},"started_at":{"type":"string","format":"date-time","description":"Deployment start timestamp"},"completed_at":{"type":"string","format":"date-time","nullable":true,"description":"Deployment completion timestamp"},"timeout_at":{"type":"string","format":"date-time","description":"Deployment timeout deadline (start + 30 minutes by default)"},"duration_ms":{"type":"integer","nullable":true,"description":"Total deployment duration in milliseconds (null until completed)"},"error_message":{"type":"string","nullable":true,"description":"Error message if deployment failed"},"error_phase":{"type":"string","nullable":true,"description":"Phase in which the deployment failed"},"metadata":{"type":"object","nullable":true,"description":"Additional deployment metadata"},"bundle_version":{"type":"string","nullable":true,"description":"Version identifier of the deployed bundle (git SHA or timestamp)"},"deployment_url":{"type":"string","nullable":true,"description":"URL where the bundle was deployed (e.g., S3 path)"},"environment":{"type":"string","nullable":true,"maxLength":100,"description":"Target environment for bundle promotion"},"restore_of_run_id":{"type":"string","nullable":true,"minLength":16,"maxLength":16,"description":"16-character `run_id` of the deployment this run was restored from\n(EA-671). Null on non-restore runs. Lifted from `metadata.restore_of_run_id`\nfor list-rendering convenience.\n"},"restore_mode":{"type":"string","nullable":true,"enum":["artifact","rebuild_from_sha"],"description":"Set on restore runs (EA-671). `artifact` indicates the prior bundle\nbytes were copied verbatim from the customer's bucket;\n`rebuild_from_sha` indicates the prior commit_sha was re-built\nbecause the artifact had been collected. Null on non-restore runs.\n"},"restore_diff_summary":{"allOf":[{"$ref":"#/components/schemas/RestoreDiffSummary"}],"nullable":true,"description":"Structured diff between the active bundle and the bundle being\nrestored, captured at confirmation time (EA-671). Always null on\nnon-restore runs. Always null in `rebuild_from_sha` mode because\nthe rebuilt bytes don't exist yet at confirmation time.\n"},"restore_warning_code":{"type":"string","nullable":true,"enum":["rebuild_bytes_mismatch"],"description":"Set on rebuild-from-SHA restores when the rebuilt bundle SHA\ndiffers from the original (EA-737). The restore still completes —\nthe build succeeded and the active pointer flips — but the bytes\nare not identical to what was originally deployed under the same\ncommit SHA (toolchain shift, dependency lockfile drift, etc.).\nNull in all other cases.\n"},"restore_bytes_match":{"type":"boolean","nullable":true,"description":"Set on rebuild-from-SHA restores (EA-737). `true` when the\nrebuilt bundle SHA matches the original, `false` when it differs,\n`null` when the original artifact row could not be located (the\nverification reference is gone). Null on non-restore runs and on\nartifact-mode restores.\n"},"restore_lineage":{"type":"array","description":"Ordered list of ancestor restores (EA-671). Empty on non-restore\nruns and on list responses. The first element is the run that\n*this* run was restored from; subsequent elements walk further back\nthrough prior restores. Only present on the detail response\n(`GET /v1/deployments/{run_id}`).\n","items":{"type":"object","required":["run_id","deployed_at","deployer_id","restored_from_run_id"],"properties":{"run_id":{"type":"string","minLength":16,"maxLength":16,"description":"Ancestor's `run_id`."},"deployed_at":{"type":"string","format":"date-time","nullable":true,"description":"Ancestor's `completed_at`, or null if it never completed / could not be loaded."},"deployer_id":{"type":"string","format":"uuid","nullable":true,"description":"User who triggered the ancestor, if any."},"restored_from_run_id":{"type":"string","nullable":true,"minLength":16,"maxLength":16,"description":"The ancestor's own `restore_of_run_id`. Null when the ancestor\nitself wasn't a restore (terminator) or when the row could not\nbe loaded.\n"},"missing":{"type":"boolean","description":"True when the ancestor row could not be loaded (deleted,\nforeign tenant, or never existed). Audit-grade signal — the\nUI can render \"unknown\" without silently dropping the\nbreadcrumb.\n"}}}},"created_at":{"type":"string","format":"date-time","description":"Creation timestamp"},"updated_at":{"type":"string","format":"date-time","description":"Last update timestamp"}}},"DeploymentResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["deployment"],"properties":{"deployment":{"$ref":"#/components/schemas/Deployment"}}}}},"DeploymentListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["deployments","pagination"],"properties":{"deployments":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}},"pagination":{"type":"object","required":["total","limit","offset","hasMore"],"properties":{"total":{"type":"integer","minimum":0,"description":"Total number of deployments"},"limit":{"type":"integer","minimum":1,"description":"Number of items per page"},"offset":{"type":"integer","minimum":0,"description":"Current offset"},"hasMore":{"type":"boolean","description":"Whether more results are available"}}}}}}},"RestorePreviewEntity":{"type":"object","required":["id","name","entity_type"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"entity_type":{"type":"string"}}},"RestorePreviewTarget":{"type":"object","required":["run_id","status","deployed_at","deployer_user_id","bundle_sha","build_id","commit_sha","storage_provider","storage_bucket","storage_key","size_bytes","retained_until","artifact_id"],"properties":{"run_id":{"type":"string"},"status":{"type":"string"},"deployed_at":{"type":"string","nullable":true},"deployer_user_id":{"type":"string","nullable":true},"bundle_sha":{"type":"string","nullable":true},"build_id":{"type":"string","nullable":true},"commit_sha":{"type":"string","nullable":true},"storage_provider":{"type":"string","nullable":true},"storage_bucket":{"type":"string","nullable":true},"storage_key":{"type":"string","nullable":true},"size_bytes":{"type":"integer","nullable":true},"retained_until":{"type":"string","format":"date-time","nullable":true},"artifact_id":{"type":"string","format":"uuid","nullable":true}}},"RestorePreviewActive":{"type":"object","required":["run_id","deployed_at","deployer_user_id","bundle_sha","build_id","storage_provider","storage_bucket","storage_key","artifact_id"],"properties":{"run_id":{"type":"string","nullable":true},"deployed_at":{"type":"string","nullable":true},"deployer_user_id":{"type":"string","nullable":true},"bundle_sha":{"type":"string"},"build_id":{"type":"string"},"storage_provider":{"type":"string"},"storage_bucket":{"type":"string"},"storage_key":{"type":"string"},"artifact_id":{"type":"string","format":"uuid"}}},"RestoreRuleDelta":{"type":"object","required":["added","removed","modified"],"properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"modified":{"type":"array","items":{"type":"string"}}}},"RestoreScopeDelta":{"type":"object","required":["added","removed"],"properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}}}},"RestoreDiffSummary":{"type":"object","required":["schema_version","target","active","partial","rules","nonHumanRules","resources","principals","featureFlags","totals"],"properties":{"schema_version":{"type":"integer","description":"Diff summary shape version (CA-001). Increment on shape change."},"target":{"type":"object","required":["bundleSha","artifactId"],"properties":{"bundleSha":{"type":"string"},"artifactId":{"type":"string","format":"uuid"}}},"active":{"type":"object","nullable":true,"required":["bundleSha","artifactId"],"properties":{"bundleSha":{"type":"string"},"artifactId":{"type":"string","format":"uuid"}}},"partial":{"type":"boolean","description":"True if either side was empty or unparseable (e.g. bytes retired)."},"rules":{"$ref":"#/components/schemas/RestoreRuleDelta"},"nonHumanRules":{"$ref":"#/components/schemas/RestoreRuleDelta"},"resources":{"$ref":"#/components/schemas/RestoreScopeDelta"},"principals":{"$ref":"#/components/schemas/RestoreScopeDelta"},"featureFlags":{"$ref":"#/components/schemas/RestoreScopeDelta"},"totals":{"type":"object","required":["target","active"],"properties":{"target":{"type":"object","required":["rules","nonHumanRules"],"properties":{"rules":{"type":"integer"},"nonHumanRules":{"type":"integer"}}},"active":{"type":"object","required":["rules","nonHumanRules"],"properties":{"rules":{"type":"integer"},"nonHumanRules":{"type":"integer"}}}}}}},"RestoreEligibility":{"type":"object","required":["canRestore"],"properties":{"canRestore":{"type":"boolean"},"reason":{"type":"string","enum":["not_succeeded","is_currently_active","insufficient_privilege","artifact_missing","source_unreachable","unrecoverable"]},"restore_mode":{"type":"string","enum":["artifact","rebuild_from_sha"]}}},"RestorePreviewResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["entity","target","active","diff_summary","eligibility"],"properties":{"entity":{"$ref":"#/components/schemas/RestorePreviewEntity"},"target":{"$ref":"#/components/schemas/RestorePreviewTarget"},"active":{"allOf":[{"$ref":"#/components/schemas/RestorePreviewActive"}],"nullable":true},"diff_summary":{"allOf":[{"$ref":"#/components/schemas/RestoreDiffSummary"}],"nullable":true},"eligibility":{"$ref":"#/components/schemas/RestoreEligibility"}}}}},"TriggerRestoreRequest":{"type":"object","required":["source_run_id","acknowledgement","restore_mode"],"properties":{"source_run_id":{"type":"string","minLength":16,"maxLength":16,"description":"16-character run_id of the deployment being restored"},"acknowledgement":{"type":"string","description":"Typed-confirmation gate. MUST equal the literal string `RESTORE`\n(case-sensitive, no trim). Any other value returns 400\n`acknowledgement_mismatch`.\n","example":"RESTORE"},"restore_mode":{"type":"string","enum":["artifact","rebuild_from_sha"],"description":"MUST match the eligibility decision returned by the\n`restore-preview` endpoint at submit time. A mismatch\nreturns 409 `mode_mismatch`.\n"}}},"TriggerRestoreResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["run_id","status"],"properties":{"run_id":{"type":"string","minLength":16,"maxLength":16,"description":"16-character run_id of the newly-created restore deployment"},"status":{"type":"string","enum":["pending"],"description":"Status of the new deployment run on creation"}}}}},"DeploymentStatsResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["initiated","in_progress","successful","failed","success_rate","failure_rate"],"properties":{"initiated":{"type":"integer","minimum":0,"description":"Cumulative total of deployment runs created across all statuses (all time)"},"in_progress":{"type":"integer","minimum":0,"description":"Currently running deployments (pending + in_progress)"},"successful":{"type":"integer","minimum":0,"description":"Number of successful deployments"},"failed":{"type":"integer","minimum":0,"description":"Number of failed deployments (failed + timeout)"},"success_rate":{"type":"number","minimum":0,"maximum":100,"description":"Success rate as percentage of terminal deployments (0 when no terminal deployments exist)"},"failure_rate":{"type":"number","minimum":0,"maximum":100,"description":"Failure rate as percentage of terminal deployments (0 when no terminal deployments exist)"}}}}},"RunningDeploymentsResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["deployments","count"],"properties":{"deployments":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}},"count":{"type":"integer","minimum":0,"description":"Number of running deployments"}}}}},"PolicyLog":{"type":"object","required":["id","tenant_id","entity_id","timestamp","level","message"],"properties":{"id":{"type":"string","format":"uuid","description":"Unique log identifier"},"tenant_id":{"type":"string","format":"uuid","description":"Tenant identifier"},"entity_id":{"type":"string","format":"uuid","description":"Entity identifier"},"run_id":{"type":"string","minLength":16,"maxLength":16,"description":"Deployment run identifier"},"timestamp":{"type":"string","format":"date-time","description":"Log timestamp"},"level":{"type":"string","enum":["debug","info","warn","error"],"description":"Log level"},"message":{"type":"string","description":"Log message"},"metadata":{"type":"object","nullable":true,"description":"Additional log metadata"}}},"PolicyLogListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["logs"],"properties":{"logs":{"type":"array","items":{"$ref":"#/components/schemas/PolicyLog"}},"next_token":{"type":"string","nullable":true,"description":"Token for fetching next page"}}}}},"WebhookPayload":{"type":"object","description":"GitHub/GitLab webhook payload for policy changes","additionalProperties":true},"WebhookResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["run_id"],"properties":{"run_id":{"type":"string","minLength":16,"maxLength":16,"description":"Deployment run identifier"}}}}},"SecretListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/SecretMetadata"}}}},"AllSecretsListResponse":{"type":"object","description":"Response containing all secrets grouped by entity","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","required":["entityId","entityType","entityName","secrets"],"properties":{"entityId":{"type":"string","format":"uuid","description":"Entity identifier"},"entityType":{"type":"string","enum":["org","system"],"description":"Entity type"},"entityName":{"type":"string","description":"Entity name"},"secrets":{"type":"array","items":{"type":"object","required":["secretName","locator","provider","createdAt","updatedAt"],"properties":{"secretName":{"type":"string","description":"Secret name/key"},"locator":{"type":"string","description":"Provider-specific locator (e.g., ARN)"},"provider":{"type":"string","description":"Secret provider (e.g., aws_secrets_manager)"},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp"},"updatedAt":{"type":"string","format":"date-time","description":"Last update timestamp"}}}}}}}}},"AvailableSecretsListResponse":{"type":"object","description":"Response containing available secrets for an entity (including ancestors)","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/AvailableSecret"}}}},"AvailableSecret":{"type":"object","description":"Secret available to an entity with entity context","required":["secretName","locator","provider","entityId","entityName","entityType","createdAt","updatedAt"],"properties":{"secretName":{"type":"string","description":"Secret name/key"},"locator":{"type":"string","description":"Provider-specific locator (e.g., ARN)"},"provider":{"type":"string","description":"Secret provider (e.g., aws_secrets_manager)"},"entityId":{"type":"string","format":"uuid","description":"Entity ID where the secret is stored"},"entityName":{"type":"string","description":"Entity name where the secret is stored"},"entityType":{"type":"string","enum":["org","system"],"description":"Entity type where the secret is stored"},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp"},"updatedAt":{"type":"string","format":"date-time","description":"Last update timestamp"}}},"InviteValidationResult":{"type":"object","description":"Result of validating an invitation token","required":["valid","email","organizationName","role","expiresAt","isExistingUser"],"properties":{"valid":{"type":"boolean","description":"Whether the invitation token is valid"},"email":{"type":"string","format":"email","description":"Email address the invitation was sent to"},"organizationName":{"type":"string","description":"Name of the organization being joined"},"role":{"type":"string","enum":["administrator","engineer"],"description":"Role assigned in the invitation"},"expiresAt":{"type":"string","format":"date-time","description":"When the invitation expires"},"isExistingUser":{"type":"boolean","description":"Whether the invitee already has an account"}}},"InviteAcceptRequest":{"type":"object","description":"Request body for accepting an invitation","required":["token"],"properties":{"token":{"type":"string","minLength":1,"description":"The invitation token from the email link"},"firstName":{"type":"string","description":"First name (required for new users)"},"lastName":{"type":"string","description":"Last name (required for new users)"},"password":{"type":"string","minLength":8,"description":"Password (required for new users)"}}},"InviteAcceptResult":{"type":"object","description":"Result of accepting an invitation","required":["userId","organizationId","role","redirectUrl"],"properties":{"userId":{"type":"string","format":"uuid","description":"User entity ID"},"organizationId":{"type":"string","format":"uuid","description":"Organization entity ID"},"role":{"type":"string","enum":["administrator","engineer"],"description":"Assigned role in the organization"},"redirectUrl":{"type":"string","description":"URL to redirect the user to after acceptance"}}},"DemoTemplateSummary":{"type":"object","description":"Summary of a demo template for listing","required":["id","name","description","version"],"properties":{"id":{"type":"string","description":"Template identifier (e.g., 'ea-financial')"},"name":{"type":"string","description":"Human-readable template name"},"description":{"type":"string","description":"Template description"},"version":{"type":"string","description":"Template version"}}},"DemoTemplateListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DemoTemplateSummary"}}}},"ApplyDemoTemplateRequest":{"type":"object","description":"Request to apply a demo template","required":["templateId"],"properties":{"templateId":{"type":"string","minLength":1,"maxLength":50,"description":"Template ID to apply (e.g., 'ea-financial')"}}},"ApplyDemoTemplateResult":{"type":"object","description":"Result of applying a demo template","required":["templateId","templateName","entitiesDeleted","entitiesCreated","configsApplied"],"properties":{"templateId":{"type":"string","description":"Applied template ID"},"templateName":{"type":"string","description":"Applied template name"},"entitiesDeleted":{"type":"integer","minimum":0,"description":"Number of existing entities deleted"},"entitiesCreated":{"type":"integer","minimum":0,"description":"Number of new entities created"},"configsApplied":{"type":"integer","minimum":0,"description":"Number of configurations applied"}}},"ApplyDemoTemplateResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/ApplyDemoTemplateResult"}}},"ApiKey":{"type":"object","required":["id","entity_id","name","scopes","key_prefix","created_at"],"properties":{"id":{"type":"string","format":"uuid","example":"770e8400-e29b-41d4-a716-446655440000"},"entity_id":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"entity_name":{"type":"string","description":"Name of the entity this key belongs to","example":"Acme Corp"},"entity_type":{"type":"string","description":"Type of entity (org or system)","example":"org"},"name":{"type":"string","maxLength":255,"example":"Production OPA Agent"},"description":{"type":"string","maxLength":1000,"nullable":true,"example":"API key for production OPA decision logging"},"scopes":{"type":"array","items":{"type":"string","enum":["decisions:write","decisions:read","status:write","status:read","policies:read"]},"example":["decisions:write","status:write"]},"key_prefix":{"type":"string","description":"First characters of the key for identification (up to 20 characters)","example":"ea_live_abc123def456"},"created_at":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"},"expires_at":{"type":"string","format":"date-time","nullable":true,"example":"2026-01-15T09:30:00.000Z"},"last_used_at":{"type":"string","format":"date-time","nullable":true,"example":"2025-01-20T14:22:00.000Z"},"revoked_at":{"type":"string","format":"date-time","nullable":true}}},"CreateApiKeyRequest":{"type":"object","required":["name","scopes"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":1000},"scopes":{"type":"array","minItems":1,"items":{"type":"string","enum":["decisions:write","decisions:read","status:write","status:read","policies:read"]}},"expires_at":{"type":"string","format":"date-time","description":"Optional expiration timestamp"}}},"UpdateApiKeyRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":1000},"expires_at":{"type":"string","format":"date-time","nullable":true}}},"CreateApiKeyResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","key","entity_id","entity_name","entity_type","name","scopes","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string","description":"The full API key (ONLY returned on creation - save it securely)"},"entity_id":{"type":"string","format":"uuid"},"entity_name":{"type":"string"},"entity_type":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"scopes":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","nullable":true}}}}},"ApiKeyResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/ApiKey"}}},"ApiKeyListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}},"next_cursor":{"type":"string","nullable":true,"description":"Cursor for pagination"}}}}},"EvaluatedFlag":{"type":"object","description":"Per-flag evaluation result for a (tenantId, userId) caller.\n`source` records which precedence tier the `enabled` value\ncame from: `user` > `tenant` > `default`.\n","required":["enabled","source"],"properties":{"enabled":{"type":"boolean"},"source":{"type":"string","enum":["default","tenant","user"]}}},"FlagEvaluation":{"type":"object","description":"Authenticated flag-evaluation response, wrapped in the standard\nsuccess envelope produced by `reply.success(...)`. The bag of\nper-flag results is keyed by `flagKey` and lives at `data.flags`.\nAlways returned with HTTP 200 — on internal error the server\nreturns `data.flags = {}` so the frontend's DEFAULT_FLAGS\nregistry kicks in.\n","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["flags"],"properties":{"flags":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/EvaluatedFlag"}}}}}},"PublicFlagMap":{"type":"object","description":"Unauthenticated marketing-scope flag map, wrapped in the\nstandard success envelope. Defaults only — no overrides apply\nat marketing scope. CloudFront caches the entire response with\nCache-Control public, max-age=30, s-maxage=30 on success and\nmax-age=5 on error.\n","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["flags"],"properties":{"flags":{"type":"object","additionalProperties":{"type":"boolean"}}}}}},"FeatureFlag":{"type":"object","description":"A flag definition (global default). Returned by the admin CRUD\nendpoints. The single-row CRUD responses do NOT include\n`overrideCount`; only the list response does.\n","required":["id","flagKey","scope","defaultEnabled","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","example":"770e8400-e29b-41d4-a716-446655440000"},"flagKey":{"type":"string","maxLength":128,"example":"policy-create"},"scope":{"type":"string","enum":["product","marketing","admin"],"example":"product"},"defaultEnabled":{"type":"boolean","example":false},"description":{"type":"string","maxLength":1000,"nullable":true,"example":"Gates Create Policy button/page (EA-411)"},"createdAt":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"},"updatedAt":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"},"createdBy":{"type":"string","format":"uuid","nullable":true},"overrideCount":{"type":"integer","minimum":0,"description":"Per-flag count of currently-configured overrides. Present\nONLY on the admin list response.\n","example":12}}},"FeatureFlagOverride":{"type":"object","description":"A per-tenant or per-user override of the global default. The\n(tenantId, flagKey, scopeType, scopeId) tuple is unique.\n","required":["id","tenantId","flagKey","scopeType","scopeId","enabled","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"flagKey":{"type":"string","maxLength":128},"scopeType":{"type":"string","enum":["tenant","user"]},"scopeId":{"type":"string","format":"uuid"},"enabled":{"type":"boolean"},"reason":{"type":"string","maxLength":1000,"nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","format":"uuid","nullable":true}}},"ListFeatureFlagsResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["flags"],"properties":{"flags":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/FeatureFlag"},{"type":"object","required":["overrideCount"],"properties":{"overrideCount":{"type":"integer","minimum":0}}}]}}}}}},"FeatureFlagResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["flag"],"properties":{"flag":{"$ref":"#/components/schemas/FeatureFlag"}}}}},"ListFeatureFlagOverridesResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["overrides"],"properties":{"overrides":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagOverride"}}}}}},"FeatureFlagOverrideResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["override"],"properties":{"override":{"$ref":"#/components/schemas/FeatureFlagOverride"}}}}},"CreateFlagRequest":{"type":"object","description":"POST /v1/feature-flags/admin body. Field names match the\nbackend Zod schema (snake_case).\n","required":["flag_key","default_enabled"],"properties":{"flag_key":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9_-]+$","description":"Lowercase letters, digits, underscores, hyphens only.\n"},"scope":{"type":"string","enum":["product","marketing","admin"],"default":"product"},"default_enabled":{"type":"boolean"},"description":{"type":"string","maxLength":1000}}},"UpdateFlagRequest":{"type":"object","description":"PATCH /v1/feature-flags/admin/{flagKey} body. At least one of\n`default_enabled` or `description` must be provided; empty\npatches are rejected with 400.\n","properties":{"default_enabled":{"type":"boolean"},"description":{"type":"string","maxLength":1000,"nullable":true}}},"SetOverrideRequest":{"type":"object","description":"PUT /v1/feature-flags/admin/{flagKey}/override body. For\n`scope_type='tenant'`, `scope_id` MUST equal the caller's\ntenantId; the server rejects mismatches with 403.\n","required":["scope_type","scope_id","enabled"],"properties":{"scope_type":{"type":"string","enum":["tenant","user"]},"scope_id":{"type":"string","format":"uuid"},"enabled":{"type":"boolean"},"reason":{"type":"string","maxLength":1000}}},"GitFileTreeNode":{"type":"object","required":["name","path","type"],"properties":{"name":{"type":"string","description":"File or directory name"},"path":{"type":"string","description":"Full path within repository"},"type":{"type":"string","enum":["file","dir"],"description":"Whether this is a file or directory"},"children":{"type":"array","items":{"$ref":"#/components/schemas/GitFileTreeNode"},"description":"Child nodes (only for directories)"}}},"PolicyFilesTreeResponse":{"type":"object","description":"Policy file tree response. The `storage_type` field acts as a\ndiscriminator — only the storage-specific fields for that type\nwill be present in the response. Common fields (`success`,\n`entity_id`, `storage_type`, `policy_path`, `tree`) are always\npresent.\n\nStorage-specific field sets:\n  - `git`:   `repository_url`, `branch`\n  - `s3`:    `bucket`, `region`\n  - `gcs`:   `bucket`, `project_id`\n  - `azure`: `container_name`, `storage_account_name`\n\nSDK consumers using strict typing should narrow on `storage_type`\nbefore reading variant-specific fields.\n","required":["success","entity_id","storage_type","policy_path","tree"],"properties":{"success":{"type":"boolean","enum":[true]},"entity_id":{"type":"string","format":"uuid"},"storage_type":{"type":"string","enum":["git","s3","gcs","azure"],"description":"The storage backend serving these policy files (discriminator)"},"policy_path":{"type":"string","description":"The path prefix within the storage where policies live"},"tree":{"type":"array","items":{"$ref":"#/components/schemas/GitFileTreeNode"}},"repository_url":{"type":"string","format":"uri","description":"Git repository URL (only present when `storage_type` is `git`)"},"branch":{"type":"string","description":"Git branch (only present when `storage_type` is `git`)"},"bucket":{"type":"string","description":"Storage bucket name (present when `storage_type` is `s3` or\n`gcs`)\n"},"region":{"type":"string","description":"AWS region (only present when `storage_type` is `s3`)"},"project_id":{"type":"string","description":"GCP project ID (only present when `storage_type` is `gcs`)"},"container_name":{"type":"string","description":"Azure container name (only present when `storage_type` is `azure`)"},"storage_account_name":{"type":"string","description":"Azure storage account name (only present when `storage_type` is `azure`)"}}},"PolicyFileContentResponse":{"type":"object","required":["success","entity_id","path","content"],"properties":{"success":{"type":"boolean","enum":[true]},"entity_id":{"type":"string","format":"uuid"},"path":{"type":"string"},"content":{"type":"string","description":"File content (Rego policy code)"}}},"PolicyIndexModule":{"type":"object","required":["file","package"],"properties":{"file":{"type":"string","description":"Relative file path (e.g., retail/accounts.rego)"},"package":{"type":"string","description":"OPA package name (e.g., retail.accounts)"},"rego_version":{"type":"integer","description":"Rego language version"},"rules":{"type":"array","items":{"type":"string"},"description":"Rule names defined in this module"}}},"PolicyIndexBody":{"type":"object","required":["schema_version","revision","built_at","modules"],"properties":{"schema_version":{"type":"integer"},"revision":{"type":"string","maxLength":255},"built_at":{"type":"string","format":"date-time"},"bundle_name":{"type":"string"},"environment":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$","description":"Target environment name (EA-508). Omit for entities without environment config."},"modules":{"type":"array","items":{"$ref":"#/components/schemas/PolicyIndexModule"}}}},"PolicyIndexResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","entity_id","bundle_revision","schema_version","index_data","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"entity_id":{"type":"string","format":"uuid"},"bundle_revision":{"type":"string"},"schema_version":{"type":"integer"},"index_data":{"type":"object","description":"Policy index data (modules, revision, etc.)"},"created_at":{"type":"string","format":"date-time"}}}}},"DriftResult":{"type":"object","required":["source_environment","target_environment","source_revision","target_revision","added","removed","modified","in_sync"],"properties":{"source_environment":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"target_environment":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"source_revision":{"type":"string"},"target_revision":{"type":"string"},"added":{"type":"array","items":{"type":"string"},"description":"Modules in source but not in target"},"removed":{"type":"array","items":{"type":"string"},"description":"Modules in target but not in source"},"modified":{"type":"array","items":{"type":"string"},"description":"Modules in both but with different content"},"in_sync":{"type":"boolean","description":"True if no differences detected"}}},"PromotionEvent":{"type":"object","required":["id","entity_id","source_environment","target_environment","promotion_method","status","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"entity_id":{"type":"string","format":"uuid"},"source_environment":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"target_environment":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"source_bundle_revision":{"type":"string","nullable":true},"target_bundle_revision":{"type":"string","nullable":true},"promoted_by":{"type":"string","format":"uuid","nullable":true},"promotion_method":{"type":"string","enum":["console","api","cli","ci"]},"status":{"type":"string","enum":["pending","merged","deployed","rolled_back"]},"pr_url":{"type":"string","format":"uri","nullable":true},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time","nullable":true}}},"ClaimMappings":{"type":"object","additionalProperties":false,"description":"Claim mappings for `provider_type='oidc'` (EA-667). Maps normalized\npolicy fields to the raw JWT claim names exposed by the customer's\nIdP. `custom` is a free-form map of normalized claim name → raw\nclaim name, used in trust-policy `custom_claims` assertions.\n","properties":{"subject":{"type":"string","minLength":1,"maxLength":100,"description":"JWT claim name to use as subject (default `sub`)","example":"sub"},"repository":{"type":"string","minLength":1,"maxLength":100,"example":"repo"},"branch":{"type":"string","minLength":1,"maxLength":100,"example":"ref"},"actor":{"type":"string","minLength":1,"maxLength":100,"example":"actor"},"custom":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":100},"description":"Map of normalized claim name → raw JWT claim name","example":{"department":"department"}}}},"CIProvider":{"type":"object","required":["id","provider_type","name","issuer","jwks_uri","enabled","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid","example":"990e8400-e29b-41d4-a716-446655440000"},"provider_type":{"type":"string","enum":["github","oidc","gitlab","circleci","jenkins"],"example":"github"},"name":{"type":"string","maxLength":255,"example":"GitHub Actions"},"issuer":{"type":"string","description":"OIDC issuer URL","example":"https://token.actions.githubusercontent.com"},"jwks_uri":{"type":"string","description":"JWKS endpoint URL","example":"https://token.actions.githubusercontent.com/.well-known/jwks"},"audience":{"type":"string","nullable":true,"description":"Expected audience claim","example":"enforceauth"},"claim_mappings":{"allOf":[{"$ref":"#/components/schemas/ClaimMappings"}],"nullable":true,"description":"Optional explicit claim mappings for `provider_type='oidc'`; null\nmeans no per-provider overrides were configured. Built-in providers\n(e.g., `github`) return null because their mappings are fixed.\n"},"enabled":{"type":"boolean","example":true},"created_at":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"},"updated_at":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"}}},"CreateCIProviderRequest":{"description":"For `provider_type='oidc'`, `issuer`, `jwks_uri`, and `audience` are\nrequired and must be provided in the request. For built-in provider\ntypes (e.g., `github`) those fields are derived from defaults and\nmust not be supplied.\n","oneOf":[{"$ref":"#/components/schemas/CreateBuiltInCIProviderRequest"},{"$ref":"#/components/schemas/CreateGenericOIDCProviderRequest"}]},"CreateBuiltInCIProviderRequest":{"type":"object","additionalProperties":false,"required":["provider_type"],"properties":{"provider_type":{"type":"string","enum":["github","gitlab","circleci","jenkins"]},"name":{"type":"string","minLength":1,"maxLength":255},"enabled":{"type":"boolean","default":true}}},"CreateGenericOIDCProviderRequest":{"type":"object","additionalProperties":false,"required":["provider_type","issuer","jwks_uri","audience"],"properties":{"provider_type":{"type":"string","enum":["oidc"]},"name":{"type":"string","minLength":1,"maxLength":255},"issuer":{"type":"string","format":"uri","minLength":1,"maxLength":500},"jwks_uri":{"type":"string","format":"uri","minLength":1,"maxLength":500},"audience":{"type":"string","minLength":1,"maxLength":500},"claim_mappings":{"$ref":"#/components/schemas/ClaimMappings"},"enabled":{"type":"boolean","default":true}}},"UpdateCIProviderRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"audience":{"type":"string","maxLength":500,"nullable":true},"claim_mappings":{"allOf":[{"$ref":"#/components/schemas/ClaimMappings"}],"nullable":true,"description":"Pass null to clear claim mappings"},"enabled":{"type":"boolean"}}},"CIProviderResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/CIProvider"}}},"CIProviderListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["providers","supported_types"],"properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/CIProvider"}},"supported_types":{"type":"array","items":{"type":"string"},"description":"List of supported CI provider types"}}}}},"TenantGithubIntegration":{"type":"object","description":"A tenant's installed GitHub App used as a tier-2 credential source for\n`policy_source` git access (EA-687). Internal storage holds an\n`installation_metadata` blob with installation metadata; that field is\ndeliberately NOT exposed on the API. Only stable identifiers and\nnon-secret installation context are returned.\n","required":["id","provider","credential_kind","host","external_id","account_login","account_type","repository_selection","installed_by_user_id","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","enum":["github"],"description":"Provider identifier. Currently always `github`."},"credential_kind":{"type":"string","enum":["github_app_install"],"description":"Credential kind. Currently always `github_app_install`."},"host":{"type":"string","description":"Git host the App is installed on — `github.com` for GitHub.com, or\na GitHub Enterprise Server hostname (e.g. `github.acme.corp`).\n","example":"github.com"},"external_id":{"type":"string","description":"GitHub installation ID (opaque string).","example":"54321678"},"account_login":{"type":"string","description":"GitHub account login (org or user the App is installed on).","example":"acme-corp"},"account_type":{"type":"string","description":"GitHub account type — `Organization` or `User`.","example":"Organization"},"repository_selection":{"type":"string","enum":["all","selected"],"description":"Whether the installation grants access to all repos in the account\nor only a selected subset.\n"},"installed_by_user_id":{"type":"string","format":"uuid","nullable":true,"description":"User who completed the install flow, if known."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TenantGithubIntegrationListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["integrations"],"properties":{"integrations":{"type":"array","items":{"$ref":"#/components/schemas/TenantGithubIntegration"}}}}}},"TrustPolicy":{"type":"object","required":["id","ci_provider_id","entity_id","name","scopes","enabled","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid","example":"aa0e8400-e29b-41d4-a716-446655440000"},"ci_provider_id":{"type":"string","format":"uuid","example":"990e8400-e29b-41d4-a716-446655440000"},"entity_id":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"name":{"type":"string","maxLength":255,"example":"Main branch deploy"},"description":{"type":"string","maxLength":1000,"nullable":true,"example":"Allow deployments from main branch only"},"subject_pattern":{"type":"string","maxLength":500,"nullable":true,"description":"Pattern to match OIDC subject claim","example":"repo:acme/policies:ref:refs/heads/main"},"repository_pattern":{"type":"string","maxLength":500,"nullable":true,"description":"Pattern to match repository (e.g., \"my-org/*\")","example":"acme/*"},"branch_pattern":{"type":"string","maxLength":500,"nullable":true,"description":"Pattern to match branch (e.g., \"refs/heads/main\")","example":"refs/heads/main"},"environment":{"type":"string","maxLength":255,"nullable":true,"description":"Required GitHub environment name","example":"production"},"actor_pattern":{"type":"string","maxLength":500,"nullable":true,"description":"Pattern to match actor/user"},"custom_claims":{"type":"object","nullable":true,"maxProperties":64,"additionalProperties":{"type":"string","minLength":1,"maxLength":500},"description":"Custom-claim glob assertions (EA-667). Map of normalized claim\nname → glob pattern. Keys must match a `claim_mappings.custom`\nentry on the CI provider. Only used when the provider is\n`provider_type='oidc'`.\n","example":{"department":"infrastructure"}},"scopes":{"type":"array","items":{"type":"string","enum":["pipeline:deploy","pipeline:read"]},"description":"Scopes granted when policy matches","example":["pipeline:deploy"]},"enabled":{"type":"boolean","example":true},"created_at":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"},"updated_at":{"type":"string","format":"date-time","example":"2025-01-15T09:30:00.000Z"}}},"CreateTrustPolicyRequest":{"type":"object","required":["ci_provider_id","entity_id","name","scopes"],"properties":{"ci_provider_id":{"type":"string","format":"uuid"},"entity_id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":1000},"subject_pattern":{"type":"string","maxLength":500},"repository_pattern":{"type":"string","maxLength":500},"branch_pattern":{"type":"string","maxLength":500},"environment":{"type":"string","maxLength":255},"actor_pattern":{"type":"string","maxLength":500},"custom_claims":{"type":"object","maxProperties":64,"additionalProperties":{"type":"string","minLength":1,"maxLength":500},"description":"Custom-claim glob assertions (EA-667). Keys are identifier-shaped\n(`/^[a-zA-Z][a-zA-Z0-9_]*$/`, max 64 chars) and must match a\n`claim_mappings.custom` entry on the CI provider.\n"},"scopes":{"type":"array","minItems":1,"items":{"type":"string","enum":["pipeline:deploy","pipeline:read"]}},"enabled":{"type":"boolean","default":true}}},"UpdateTrustPolicyRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":1000,"description":"Empty string clears the field"},"subject_pattern":{"type":"string","maxLength":500,"description":"Empty string clears the field"},"repository_pattern":{"type":"string","maxLength":500,"description":"Empty string clears the field"},"branch_pattern":{"type":"string","maxLength":500,"description":"Empty string clears the field"},"environment":{"type":"string","maxLength":255,"description":"Empty string clears the field"},"actor_pattern":{"type":"string","maxLength":500,"description":"Empty string clears the field"},"custom_claims":{"type":"object","nullable":true,"maxProperties":64,"additionalProperties":{"type":"string","minLength":1,"maxLength":500},"description":"Pass null to clear all custom claims. Replaces the entire map\n(no partial merge).\n"},"scopes":{"type":"array","minItems":1,"items":{"type":"string","enum":["pipeline:deploy","pipeline:read"]}},"enabled":{"type":"boolean"}}},"TrustPolicyResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/TrustPolicy"}}},"TrustPolicyListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["policies"],"properties":{"policies":{"type":"array","items":{"$ref":"#/components/schemas/TrustPolicy"}}}}}},"OIDCTokenExchangeResponse":{"type":"object","required":["access_token","issued_token_type","token_type","expires_in"],"properties":{"access_token":{"type":"string","description":"The issued access token"},"issued_token_type":{"type":"string","enum":["urn:ietf:params:oauth:token-type:access_token"]},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer","description":"Token lifetime in seconds"}}},"OIDCTokenExchangeError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["invalid_request","invalid_grant","unsupported_grant_type","access_denied"],"description":"OAuth 2.0 error code"},"error_description":{"type":"string","description":"Human-readable error description"}}},"OIDCDiscoveryDocument":{"type":"object","description":"OpenID Connect discovery metadata document for the EnforceAuth OIDC provider","required":["issuer","token_endpoint","grant_types_supported"],"properties":{"issuer":{"type":"string","format":"uri","description":"The OIDC issuer URL (matches the `iss` claim in issued tokens)"},"token_endpoint":{"type":"string","format":"uri","description":"URL of the token endpoint for token exchange requests"},"token_endpoint_auth_methods_supported":{"type":"array","description":"Supported client authentication methods at the token endpoint","items":{"type":"string"}},"grant_types_supported":{"type":"array","description":"Supported OAuth 2.0 grant types (e.g., urn:ietf:params:oauth:grant-type:token-exchange)","items":{"type":"string"}},"subject_token_types_supported":{"type":"array","description":"Token types accepted as subject_token in token exchange","items":{"type":"string"}},"requested_token_types_supported":{"type":"array","description":"Token types that can be requested via requested_token_type","items":{"type":"string"}},"response_types_supported":{"type":"array","description":"Supported OAuth 2.0 response types","items":{"type":"string"}},"scopes_supported":{"type":"array","description":"Available scopes for workload identity tokens","items":{"type":"string"}}}},"OPAStatusPayload":{"type":"object","required":["labels"],"properties":{"labels":{"type":"object","properties":{"id":{"type":"string","description":"OPA instance identifier"},"version":{"type":"string","description":"OPA version"}},"additionalProperties":true,"description":"OPA instance labels"},"bundles":{"type":"object","nullable":true,"description":"Bundle status by bundle name. Values may be null.","additionalProperties":true},"plugins":{"type":"object","nullable":true,"description":"Plugin status by plugin name. Values may be null.","additionalProperties":true},"discovery":{"nullable":true,"description":"Discovery bundle status (may be null)","allOf":[{"$ref":"#/components/schemas/OPABundleStatus"}]},"decision_logs":{"nullable":true,"description":"Decision logs plugin status (may be null)","allOf":[{"$ref":"#/components/schemas/OPADecisionLogsStatus"}]},"metrics":{"type":"object","nullable":true,"description":"Metrics from OPA. Can contain arbitrary metric provider names (e.g., \"prometheus\", \"<built-in>\").\nValues may be null when metrics are not available or when using noOpMetrics.\n","additionalProperties":true}},"additionalProperties":true},"OPABundleStatus":{"type":"object","properties":{"name":{"type":"string"},"active_revision":{"type":"string"},"last_request":{"type":"string","description":"ISO 8601 timestamp (OPA may include microseconds)"},"last_successful_request":{"type":"string","description":"ISO 8601 timestamp (OPA may include microseconds)"},"last_successful_download":{"type":"string","description":"ISO 8601 timestamp (OPA may include microseconds)"},"last_successful_activation":{"type":"string","description":"ISO 8601 timestamp (OPA may include microseconds)"},"metrics":{"type":"object","additionalProperties":{"type":"number"}},"size":{"type":"integer"},"type":{"type":"string","enum":["snapshot","delta"]},"code":{"type":"string","description":"Error code if bundle has issues"},"message":{"type":"string","description":"Error message if bundle has issues"},"http_code":{"oneOf":[{"type":"string"},{"type":"integer"}]},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}},"additionalProperties":true},"OPAPluginStatus":{"type":"object","required":["state"],"properties":{"state":{"type":"string","enum":["OK","NOT_READY","ERROR"]}},"additionalProperties":true},"OPADecisionLogsStatus":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"http_code":{"oneOf":[{"type":"string"},{"type":"integer"}]},"metrics":{"type":"object","additionalProperties":{"type":"number"},"description":"Numeric metrics (counters, timers in nanoseconds)"}},"additionalProperties":true},"TimeRange":{"type":"object","description":"Time range with ISO 8601 start and end timestamps","required":["start","end"],"properties":{"start":{"type":"string","format":"date-time","description":"Start of the time range (ISO 8601)"},"end":{"type":"string","format":"date-time","description":"End of the time range (ISO 8601)"}}},"OPAHealthStatus":{"type":"string","enum":["healthy","degraded","unhealthy","unknown"],"description":"Derived health status from OPA status payload"},"OPAStatusResponse":{"type":"object","required":["id","entity_id","pdp_instance_id","health_status","reported_at","received_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"entity_id":{"type":"string","format":"uuid"},"entity_name":{"type":"string","nullable":true},"pdp_instance_id":{"type":"string","description":"PDP instance identifier (supports OPA, ArbiterPDP, etc.)"},"pdp_version":{"type":"string","nullable":true},"labels":{"type":"object","additionalProperties":{"type":"string"},"nullable":true},"health_status":{"$ref":"#/components/schemas/OPAHealthStatus"},"bundles":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/OPABundleStatus"},"nullable":true},"plugins":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/OPAPluginStatus"},"nullable":true},"discovery":{"allOf":[{"$ref":"#/components/schemas/OPABundleStatus"}],"nullable":true},"decision_logs":{"allOf":[{"$ref":"#/components/schemas/OPADecisionLogsStatus"}],"nullable":true},"primary_bundle_revision":{"type":"string","nullable":true},"plugins_healthy":{"type":"boolean","nullable":true},"reported_at":{"type":"string","format":"date-time"},"received_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"OPAStatusIngestResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","health_status","received_at"],"properties":{"id":{"type":"string","format":"uuid"},"health_status":{"$ref":"#/components/schemas/OPAHealthStatus"},"received_at":{"type":"string","format":"date-time"}}}}},"OPAFleetStatusResponse":{"type":"object","required":["success","data","summary","pagination"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/OPAStatusResponse"}},"summary":{"type":"object","required":["total","healthy","degraded","unhealthy","unknown"],"properties":{"total":{"type":"integer","minimum":0},"healthy":{"type":"integer","minimum":0},"degraded":{"type":"integer","minimum":0},"unhealthy":{"type":"integer","minimum":0},"unknown":{"type":"integer","minimum":0}}},"time_range":{"nullable":true,"description":"Time range applied to the query (null if no time filter was used)","type":"object","required":["start","end"],"properties":{"start":{"type":"string","format":"date-time","description":"Start of the time range (ISO 8601)"},"end":{"type":"string","format":"date-time","description":"End of the time range (ISO 8601)"}}},"pagination":{"type":"object","required":["cursor","has_more"],"properties":{"cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}}}}},"OPAInstanceHistoryResponse":{"type":"object","required":["success","data","pagination"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/OPAStatusResponse"}},"pagination":{"type":"object","required":["cursor","has_more"],"properties":{"cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}}}}},"PricingTier":{"type":"object","required":["id","slug","name","display_order","log_retention_days","support_level","is_active"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"tier_label":{"type":"string","nullable":true},"display_order":{"type":"integer"},"annual_rate_cents":{"type":"integer","nullable":true,"description":"Annual rate in cents (null for custom pricing)"},"max_decisions_per_month":{"type":"integer","nullable":true,"description":"Maximum decisions per month (null for unlimited)"},"overage_rate_cents_per_million":{"type":"integer","nullable":true,"description":"Overage rate in cents per million decisions"},"log_retention_days":{"type":"integer"},"reconciliation_period_days":{"type":"integer","nullable":true},"support_level":{"type":"string"},"is_active":{"type":"boolean"}}},"TenantBillingProfile":{"type":"object","required":["id","tenant_id","pricing_tier","status","contract_start_date","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"pricing_tier":{"$ref":"#/components/schemas/PricingTier"},"status":{"type":"string"},"contract_start_date":{"type":"string","format":"date-time"},"contract_end_date":{"type":"string","format":"date-time","nullable":true},"custom_max_decisions_per_month":{"type":"integer","nullable":true},"custom_overage_rate_cents_per_million":{"type":"integer","nullable":true},"custom_annual_rate_cents":{"type":"integer","nullable":true},"notes":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"AdminTenantSummary":{"type":"object","required":["id","name","owner_email","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"owner_email":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"msa_accepted_at":{"type":"string","format":"date-time","nullable":true},"billing_profile":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"pricing_tier":{"$ref":"#/components/schemas/PricingTier"},"status":{"type":"string"},"contract_start_date":{"type":"string","format":"date-time"},"contract_end_date":{"type":"string","format":"date-time","nullable":true},"custom_max_decisions_per_month":{"type":"integer","nullable":true},"notes":{"type":"string","nullable":true}}}}},"AdminTenantDetails":{"type":"object","description":"Full tenant detail payload returned by the admin drill-down endpoint.\nTop-level shape is locked; deeply nested panels (onboarding, billing,\nmembers[], msa_history[], usage_metrics, policy_stats) use\nadditionalProperties so they can evolve without breaking the contract\non this endpoint. Tighten to concrete $refs if client generators need\nstricter nested types.\n","required":["tenant","owner_email","owner_source","onboarding","billing_profile","members","msa_history","usage_metrics","policy_stats","partial","failed_subqueries"],"properties":{"tenant":{"type":"object","required":["id","name","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"owner_email":{"type":"string","nullable":true,"description":"Resolved owner email, or null when no active members exist."},"owner_source":{"type":"string","enum":["bootstrap","oldest_member","none"],"description":"Heuristic that produced owner_email."},"onboarding":{"type":"object","nullable":true,"additionalProperties":true,"description":"Parsed onboarding metadata blob, or null when missing/malformed."},"billing_profile":{"type":"object","nullable":true,"additionalProperties":true,"description":"Tenant billing profile with pricing tier, or null."},"members":{"type":"object","required":["items","has_more"],"properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":true}},"has_more":{"type":"boolean"}}},"msa_history":{"type":"object","required":["items","has_more"],"properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":true}},"has_more":{"type":"boolean"}}},"usage_metrics":{"type":"object","nullable":true,"additionalProperties":true,"description":"30-day usage metrics, or null for tenants with no hourly data yet."},"policy_stats":{"type":"object","nullable":true,"additionalProperties":true,"description":"Latest policy composition snapshot, or null when no dashboard load."},"partial":{"type":"boolean","description":"True when at least one optional sub-query failed and the caller sees a degraded response."},"failed_subqueries":{"type":"array","description":"Names of sub-queries that failed. Empty when partial is false.","items":{"type":"string"}}}},"OverageIteration":{"type":"object","required":["id","iteration","billing_ts","threshold","decisions_at_crossing","amount_cents","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"iteration":{"type":"integer","description":"1-indexed overage block number"},"billing_ts":{"type":"string","format":"date-time","description":"When this iteration was recorded (reconciliation time)"},"threshold":{"type":"integer","description":"The threshold crossed (included + iteration * 1,000,000)"},"decisions_at_crossing":{"type":"integer","description":"Actual total_decisions when this was detected"},"overage_rate_cents_per_million":{"type":"integer","nullable":true,"description":"The overage rate in effect at recording time"},"amount_cents":{"type":"integer","description":"Per-iteration charge (1 * overage_rate or 0)"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"MonthlyUsageSummary":{"type":"object","required":["id","tenant_id","billing_month","period_start","period_end","total_decisions","overage_iterations","status","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"billing_month":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$","description":"Billing month in YYYY-MM format"},"period_start":{"type":"string","format":"date-time"},"period_end":{"type":"string","format":"date-time"},"total_decisions":{"type":"integer"},"included_decisions":{"type":"integer","nullable":true,"description":"Effective monthly decision limit at reconciliation time. Null for unlimited tiers."},"overage_iterations":{"type":"array","items":{"$ref":"#/components/schemas/OverageIteration"},"description":"Per-1MM overage blocks recorded for this billing month. Empty for unlimited tiers or when under limit."},"status":{"type":"string","enum":["provisional","reconciled"]},"reconciliation_source":{"type":"string","nullable":true},"last_reconciled_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"AssignTierRequest":{"type":"object","required":["pricing_tier_slug","contract_start_date"],"properties":{"pricing_tier_slug":{"type":"string","minLength":1,"maxLength":50},"contract_start_date":{"type":"string","format":"date-time"},"contract_end_date":{"type":"string","format":"date-time"},"custom_max_decisions_per_month":{"type":"integer","minimum":0},"custom_overage_rate_cents_per_million":{"type":"integer","minimum":0},"custom_annual_rate_cents":{"type":"integer","minimum":0},"notes":{"type":"string","maxLength":1000}}},"ContactInfo":{"description":"Relationship (primary) contact. name/email/phone are mandatory; title is optional (may be empty).","type":"object","required":["name","email","phone","countryCode"],"properties":{"name":{"type":"string","maxLength":200,"pattern":"^[a-zA-Z0-9\\s\\-'.]+$"},"title":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9\\s\\-'.&/,()]*$"},"email":{"type":"string","maxLength":254,"format":"email"},"phone":{"type":"string","maxLength":30,"pattern":"^[0-9+\\-() .]+$"},"countryCode":{"type":"string","maxLength":5,"pattern":"^\\+\\d{1,4}$"}}},"BillingContactInfo":{"allOf":[{"$ref":"#/components/schemas/ContactInfo"},{"type":"object","required":["address"],"properties":{"address":{"type":"string","maxLength":500,"pattern":"^[a-zA-Z0-9\\s\\-'.,#/&()]*$"}}}]},"OptionalContactInfo":{"description":"Optional (technical/billing) contact. Every field is individually optional and may be empty — the wizard only collects these when the user opts into the technical/billing gate, and any field may be left blank.","type":"object","properties":{"name":{"type":"string","maxLength":200,"pattern":"^[a-zA-Z0-9\\s\\-'.]*$"},"title":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9\\s\\-'.&/,()]*$"},"email":{"type":"string","maxLength":254,"description":"A valid email address, or an empty string."},"phone":{"type":"string","maxLength":30,"pattern":"^[0-9+\\-() .]*$"},"countryCode":{"type":"string","maxLength":5,"description":"A valid +NNNN calling code, or an empty string.","pattern":"^(\\+\\d{1,4})?$"}}},"OptionalBillingContactInfo":{"allOf":[{"$ref":"#/components/schemas/OptionalContactInfo"},{"type":"object","properties":{"address":{"type":"string","maxLength":500,"pattern":"^[a-zA-Z0-9\\s\\-'.,#/&()]*$"}}}]},"UserGroup":{"type":"object","required":["id","tenantId","name","description","createdAt","createdBy","updatedAt","memberCount"],"properties":{"id":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","nullable":true,"maxLength":255},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"memberCount":{"type":"integer","minimum":0,"description":"Number of users currently assigned to this group. Always 0 until membership ships."}}},"UserGroupCreate":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","maxLength":255}}},"UserGroupUpdate":{"type":"object","minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","maxLength":255}}},"UserGroupResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/UserGroup"}}},"UserGroupListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/UserGroup"}}}},"UserGroupHasMembersError":{"type":"object","required":["success","error","message","meta"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","enum":["group_has_members"]},"message":{"type":"string"},"meta":{"type":"object","required":["memberCount"],"properties":{"memberCount":{"type":"integer","minimum":1}}}}},"GroupMembershipUserIds":{"type":"object","required":["userIds"],"properties":{"userIds":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","format":"uuid"}}}},"GroupMembershipMember":{"type":"object","required":["userId","assignedAt","assignedBy"],"properties":{"userId":{"type":"string","format":"uuid"},"assignedAt":{"type":"string","format":"date-time"},"assignedBy":{"type":"string"}}},"GroupMembershipListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["members"],"properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/GroupMembershipMember"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; absent when no further pages remain."}}}}},"GroupMembershipAddResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["added","alreadyMembers"],"properties":{"added":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs that were newly added in this call."},"alreadyMembers":{"type":"array","items":{"type":"string","format":"uuid"},"description":"User IDs that were already members. Add is idempotent — re-issuing\nthe same request will move all `added` IDs into `alreadyMembers`.\n"}}}}},"GroupMembershipBulkRemoveResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["removed"],"properties":{"removed":{"type":"array","items":{"type":"string","format":"uuid"}}}}}},"GroupMembershipUserNotInTenantError":{"type":"object","required":["success","error","message","meta"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","enum":["user_not_in_tenant"]},"message":{"type":"string"},"meta":{"type":"object","required":["invalidUserIds"],"properties":{"invalidUserIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}},"GroupMembershipNotAMemberError":{"type":"object","required":["success","error","message","meta"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","enum":["not_a_member"]},"message":{"type":"string"},"meta":{"type":"object","required":["notMembers"],"properties":{"notMembers":{"type":"array","items":{"type":"string","format":"uuid"}}}}}},"GroupSummary":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","maxLength":80}}},"UserGroupMembershipsLookupResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/GroupSummary"}},"description":"Map of userId → list of groups the user belongs to. Users with zero\nmemberships have an empty array (a valid state — see EA-869 AC).\n"}}},"TenantUserStatus":{"type":"string","enum":["Active","Pending","Dormant","Deactivated","NoAccess"],"description":"Server-derived user status (EA-896). See `tenant-users/status.ts` for the\nderivation rules. Pill set is sourced from Figma 2136:12897.\n"},"TenantUserEntityMembership":{"type":"object","required":["entityId","entityName","role"],"properties":{"entityId":{"type":"string","format":"uuid"},"entityName":{"type":"string"},"role":{"type":"string"}}},"TenantUser":{"type":"object","required":["id","name","email","status","joinedAt","lastLoginAt","groupMemberships","entityMemberships"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"status":{"$ref":"#/components/schemas/TenantUserStatus"},"joinedAt":{"type":"string","format":"date-time","nullable":true,"description":"Earliest active membership joinedAt (null when no active relations exist)."},"lastLoginAt":{"type":"string","format":"date-time","nullable":true},"groupMemberships":{"type":"array","items":{"$ref":"#/components/schemas/GroupSummary"}},"entityMemberships":{"type":"array","items":{"$ref":"#/components/schemas/TenantUserEntityMembership"}}}},"TenantUserListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/TenantUser"}}}},"TenantInvitation":{"type":"object","required":["id","email","role","entityId","entityName","invitedBy","invitedAt","expiresAt","userId"],"properties":{"id":{"type":"string","format":"uuid","description":"entity_relations row id"},"email":{"type":"string","format":"email"},"role":{"type":"string"},"entityId":{"type":"string","format":"uuid","description":"Target entity (org/system) being joined"},"entityName":{"type":"string"},"invitedBy":{"type":"string","format":"uuid","description":"Platform user id of the inviter"},"invitedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time","nullable":true,"description":"ISO timestamp of expiry. Null only when the underlying relation\nmetadata was written without an expiry — pending invites should\nalways carry one but the type permits the absence.\n"},"userId":{"type":"string","format":"uuid","description":"Shell user entity created for the invite"}}},"TenantInvitationListResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/TenantInvitation"}}}}},"parameters":{"TenantIdHeader":{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Tenant identifier for multi-tenancy isolation (optional for service accounts - uses DEFAULT_TENANT_ID)"},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","format":"uuid","maxLength":255},"description":"Optional idempotency key for ensuring exactly-once processing of mutating operations.\n\n**How It Works:**\n- Client generates a unique key (typically UUID v4) for each unique operation\n- If you retry a request with the same key, you'll get the cached response without side effects\n- Keys are scoped per tenant and expire after 24 hours\n- Same key with different request body returns 422 error\n\n**Use Cases:**\n- Network retry safety (request succeeds but client never receives response)\n- UI double-click protection\n- Background job retry safety\n\n**Example:**\n```javascript\nimport { v4 as uuidv4 } from 'uuid';\n\nconst idempotencyKey = uuidv4(); // Generate once per user action\n\nfetch('/v1/entities/123/configs/policy_source', {\n  method: 'PUT',\n  headers: {\n    'Idempotency-Key': idempotencyKey,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({ config_data: {...} })\n});\n\n// If network fails, retry with SAME key\n// Server returns cached response, no duplicate created\n```\n\n**Response Headers:**\n- `X-Idempotency-Replay: true` - Returned when serving cached response\n"},"EntityId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Entity identifier"},"LogReplayJobId":{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Log replay job identifier"},"RelationId":{"name":"relationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Relation identifier"},"EntityUserId":{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User entity identifier for entity user operations"},"ConfigType":{"name":"type","in":"path","required":true,"schema":{"type":"string","enum":["policy_source","bundle_destination","environment_config"]},"description":"Configuration type"},"SecretEntityType":{"name":"entityType","in":"path","required":true,"schema":{"type":"string","enum":["org","system"]},"description":"Entity type for the secret"},"SecretEntityId":{"name":"entityId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Entity ID for the secret"},"SecretName":{"name":"secretName","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Secret name/key"},"Page":{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"Page number for pagination"},"Limit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":10},"description":"Number of items per page (max 100)"},"DecisionLogId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Decision log identifier"},"DecisionLogCursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Pagination cursor (base64 encoded)"},"DecisionLogLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000,"default":100},"description":"Number of decision logs per page (max 1000)"},"DecisionLogTimestampStart":{"name":"timestamp_start","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Filter logs from this timestamp (inclusive)"},"DecisionLogTimestampEnd":{"name":"timestamp_end","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Filter logs until this timestamp (inclusive)"},"DecisionLogPath":{"name":"path","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by policy path"},"DecisionOutcome":{"name":"decision_outcome","in":"query","required":false,"schema":{"type":"string","enum":["allow","deny","error","partial"]},"description":"Filter by decision outcome"},"DecisionLogSystemId":{"name":"system_id","in":"query","required":false,"schema":{"type":"string","maxLength":256},"description":"Filter by OPA system identifier (labels.id - UUID or k8s-style)"},"DecisionLogDecisionId":{"name":"decision_id","in":"query","required":false,"schema":{"type":"string","maxLength":256},"description":"Filter by OPA decision ID (auto-generated UUID or custom format)"},"DecisionLogBundleRevision":{"name":"bundle_revision","in":"query","required":false,"schema":{"type":"string","maxLength":256},"description":"Filter by bundle revision (git SHA, semver, or custom format)"},"DecisionLogSortBy":{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","enum":["timestamp","decision_outcome","path","system_id"],"default":"timestamp"},"description":"Field to sort results by"},"DecisionLogSortOrder":{"name":"sort_order","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Sort order (ascending or descending)"},"RunId":{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":16,"maxLength":16},"description":"Deployment run identifier (16 characters)"},"DeploymentStatus":{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","in_progress","success","failed","timeout","all"]},"description":"Filter by deployment status"},"DeploymentEntityId":{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by entity ID"},"DeploymentIncludeDescendants":{"name":"include_descendants","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Include deployments from descendant entities (for org-level views)"},"DeploymentSearch":{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":255},"description":"Search across run ID, entity name, and user ID"},"DeploymentLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Number of deployments per page (max 100)"},"DeploymentOffset":{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"Offset for pagination"},"PolicyLogLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000,"default":100},"description":"Number of policy logs per page (max 1000)"},"PolicyLogStartTime":{"name":"start_time","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Filter logs from this timestamp (inclusive)"},"PolicyLogEndTime":{"name":"end_time","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Filter logs until this timestamp (inclusive)"},"PolicyLogRunId":{"name":"run_id","in":"query","required":false,"schema":{"type":"string","minLength":16,"maxLength":16},"description":"Filter by deployment run ID"},"ApiKeyId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"API key identifier"},"FlagKey":{"name":"flagKey","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9_-]+$"},"description":"Feature flag key — lowercase letters, digits, underscores,\nhyphens only (EA-906).\n"},"CIProviderId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"CI provider identifier"},"TrustPolicyId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Trust policy identifier"},"StatusLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Number of status records per page (max 100)"},"StatusCursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Pagination cursor (ISO timestamp)"},"StatusEntityId":{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by entity ID"},"StatusHealthStatus":{"name":"health_status","in":"query","required":false,"schema":{"type":"string","enum":["healthy","degraded","unhealthy","unknown"]},"description":"Filter by health status"},"StatusPdpInstanceId":{"name":"pdp_instance_id","in":"query","required":false,"schema":{"type":"string","maxLength":255},"description":"Filter by PDP instance ID (OPA, ArbiterPDP, etc.)"},"StatusStartTime":{"name":"start","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Filter instances with received_at >= this ISO 8601 timestamp"},"StatusEndTime":{"name":"end","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Filter instances with received_at <= this ISO 8601 timestamp"},"StatusInstanceIdPath":{"name":"instanceId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"PDP instance identifier"},"StatusInstanceLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Number of history records per page (max 100)"}},"responses":{"BadRequest":{"description":"Bad request - validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"example":{"success":false,"error":"validation_error","message":"Validation failed","errors":[{"code":"invalid","message":"Invalid email format","field":"email"},{"code":"required","message":"Name is required","field":"name"}]}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"user_not_found"}}}},"IdempotencyKeyMismatch":{"description":"Idempotency key mismatch - same key used with different request body or endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"idempotency_key_mismatch","message":"Idempotency key already used with different request body"}}}},"InternalError":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"internal_server_error"}}}},"Unauthorized":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Authentication required"}}}},"Forbidden":{"description":"Access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"forbidden","message":"Not authorized to access this resource"}}}},"Conflict":{"description":"Resource conflict - duplicate or conflicting state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"conflict","message":"Resource already exists or conflicts with current state"}}}},"TooManyRequests":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"rate_limit_exceeded","message":"Too many requests. Please try again later."}}}},"OIDCTooManyRequests":{"description":"Rate limit exceeded (OIDC format)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OIDCTokenExchangeError"},"example":{"error":"invalid_request","error_description":"Too many requests. Please try again later."}}}}}},"paths":{"/v1/invite/validate":{"get":{"summary":"Validate invitation token","description":"Public endpoint to validate an invitation token from an email link.\nReturns invitation details (organization, role) if valid.\nNo authentication required.\n","operationId":"validateInviteToken","tags":["Invitations"],"security":[],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","minLength":1},"description":"The invitation token from the email link"}],"responses":{"200":{"description":"Token is valid","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/InviteValidationResult"}}},"example":{"success":true,"data":{"valid":true,"email":"john@example.com","organizationName":"Acme Corp","role":"engineer","expiresAt":"2024-12-31T23:59:59Z","isExistingUser":false}}}}},"404":{"description":"Invalid or already-used token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"invalid_token","message":"Invalid or expired invitation token"}}}},"410":{"description":"Token has expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"expired_token","message":"This invitation has expired"}}}}}}},"/v1/invite/accept":{"post":{"summary":"Accept invitation","description":"Public endpoint to accept an invitation.\nFor new users, also collects profile information.\nNo authentication required - token provides authorization.\n","operationId":"acceptInvitation","tags":["Invitations"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteAcceptRequest"},"example":{"token":"abc123xyz","firstName":"John","lastName":"Doe","password":"securePassword123"}}}},"responses":{"200":{"description":"Invitation accepted successfully","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/InviteAcceptResult"}}},"example":{"success":true,"data":{"userId":"123e4567-e89b-12d3-a456-426614174000","organizationId":"987fcdeb-51a2-3b4c-5d6e-7f8a9b0c1d2e","role":"engineer","redirectUrl":"/auth"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"description":"Invalid or already-used token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"invalid_token","message":"Invalid or expired invitation token"}}}},"410":{"description":"Token has expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"expired_token","message":"This invitation has expired"}}}}}}},"/v1/auth/oidc/token":{"post":{"summary":"Exchange OIDC token for access token","description":"RFC 8693 Token Exchange endpoint for CI/CD workload identity.\nExchanges a CI provider OIDC token (e.g., GitHub Actions `ACTIONS_ID_TOKEN_REQUEST_TOKEN`)\nfor a short-lived EnforceAuth access token.\n\nThis endpoint is PUBLIC — authentication happens via the CI provider's OIDC token.\nTrust policies control which tokens are accepted and what scopes are granted.\n\n**Typical CI/CD workflow (GitHub Actions):**\n```yaml\n- uses: actions/github-script@v7\n  id: token\n  with:\n    script: return await core.getIDToken('enforceauth')\n- run: |\n    curl -X POST https://api.enforceauth.com/v1/auth/oidc/token \\\n      -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \\\n      -d subject_token=${{ steps.token.outputs.result }} \\\n      -d subject_token_type=urn:ietf:params:oauth:token-type:jwt \\\n      -d entity_id=${{ vars.EA_ENTITY_ID }}\n```\n\n**Validation flow:**\n1. Parse the `subject_token` JWT and extract the `iss` (issuer) claim.\n2. Look up a registered CI Provider matching the issuer URL.\n3. Verify the JWT signature against the provider's JWKS endpoint.\n4. Match the token's `sub` claim against Trust Policy subject patterns.\n5. If matched, issue an access token with the trust policy's granted scopes.\n","operationId":"exchangeOIDCToken","tags":["OIDC"],"security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","subject_token","subject_token_type","entity_id"],"properties":{"grant_type":{"type":"string","enum":["urn:ietf:params:oauth:grant-type:token-exchange"],"description":"Must be token-exchange grant type"},"subject_token":{"type":"string","description":"The CI provider OIDC token (JWT)"},"subject_token_type":{"type":"string","enum":["urn:ietf:params:oauth:token-type:jwt"],"description":"Token type of subject_token"},"requested_token_type":{"type":"string","enum":["urn:ietf:params:oauth:token-type:access_token"],"description":"Desired token type (defaults to access_token)"},"entity_id":{"type":"string","format":"uuid","description":"Target entity ID to get access for"}}}}}},"responses":{"200":{"description":"Token exchange successful","headers":{"Cache-Control":{"schema":{"type":"string","enum":["no-store"]}},"Pragma":{"schema":{"type":"string","enum":["no-cache"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OIDCTokenExchangeResponse"}}}},"400":{"description":"Invalid request (malformed JWT, missing parameters)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OIDCTokenExchangeError"}}}},"403":{"description":"Access denied (no matching trust policy)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OIDCTokenExchangeError"}}}},"429":{"$ref":"#/components/responses/OIDCTooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/auth/oidc/.well-known/openid-configuration":{"get":{"summary":"OIDC Discovery Document","description":"Returns the OIDC discovery document for clients that want to discover our token endpoint.\nNote: EnforceAuth is a relying party, not a full OIDC provider.\n","operationId":"getOIDCDiscovery","tags":["OIDC"],"security":[],"responses":{"200":{"description":"OIDC discovery document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OIDCDiscoveryDocument"}}}}}}},"/v1/ci-providers":{"get":{"summary":"List CI providers","description":"List all CI providers configured for the tenant","operationId":"listCIProviders","tags":["CI Providers"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"List of CI providers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CIProviderListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Create CI provider","description":"Register a new CI provider (GitHub, GitLab, etc.) for the tenant.\nUses predefined configuration for the provider type.\n","operationId":"createCIProvider","tags":["CI Providers"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCIProviderRequest"}}}},"responses":{"201":{"description":"CI provider created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CIProviderResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Provider already exists for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/status":{"post":{"summary":"Ingest OPA status report","description":"Ingest status/telemetry report from an OPA instance.\n\nUses OPA's native status payload format - no transformation required on OPA side.\nThe status plugin in OPA can be configured to send reports directly to this endpoint.\n\nAuthentication: API key with 'status:write' scope (X-API-Key header).\nThe API key's associated entity determines which entity the status is linked to.\n\nReturns 202 Accepted as metrics and logs are processed asynchronously.\n","operationId":"ingestOPAStatus","tags":["Status"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OPAStatusPayload"},"example":{"labels":{"id":"1780d507-aea2-45cc-ae50-fa153c8e4a5a","version":"0.60.0","app":"my-example-app"},"bundles":{"authz":{"name":"authz","active_revision":"ABC123","last_successful_activation":"2024-01-15T10:30:00.000Z"}},"plugins":{"bundle":{"state":"OK"},"decision_logs":{"state":"OK"},"status":{"state":"OK"}}}}}},"responses":{"202":{"description":"Status report accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OPAStatusIngestResponse"},"example":{"success":true,"data":{"id":"550e8400-e29b-41d4-a716-446655440000","health_status":"healthy","received_at":"2024-01-15T10:30:01.000Z"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid X-API-Key header"}}}},"403":{"description":"Forbidden - API key lacks status:write scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"forbidden","message":"Insufficient permissions"}}}},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"summary":"Get fleet status","description":"Query current OPA fleet status (most recent status per OPA instance).\n\nReturns a deduplicated list showing the latest status for each unique OPA instance,\nalong with a summary of health states across the fleet.\n\nSupports filtering by entity, health status, and specific OPA instance.\nResults are paginated using cursor-based pagination.\n","operationId":"getFleetStatus","tags":["Status"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/StatusLimit"},{"$ref":"#/components/parameters/StatusCursor"},{"$ref":"#/components/parameters/StatusEntityId"},{"$ref":"#/components/parameters/StatusHealthStatus"},{"$ref":"#/components/parameters/StatusPdpInstanceId"},{"$ref":"#/components/parameters/StatusStartTime"},{"$ref":"#/components/parameters/StatusEndTime"}],"responses":{"200":{"description":"Fleet status retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OPAFleetStatusResponse"},"example":{"success":true,"data":[{"id":"550e8400-e29b-41d4-a716-446655440000","entity_id":"123e4567-e89b-12d3-a456-426614174000","entity_name":"Production API","pdp_instance_id":"1780d507-aea2-45cc-ae50-fa153c8e4a5a","pdp_version":"0.60.0","health_status":"healthy","plugins_healthy":true,"primary_bundle_revision":"ABC123","reported_at":"2024-01-15T10:30:00.000Z","received_at":"2024-01-15T10:30:01.000Z","created_at":"2024-01-15T10:30:01.000Z"}],"summary":{"total":5,"healthy":4,"degraded":1,"unhealthy":0,"unknown":0},"pagination":{"cursor":"","has_more":false}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/status/{instanceId}":{"get":{"summary":"Get OPA instance status history","description":"Get status history for a specific OPA instance.\n\nReturns a time-ordered list of status reports from the specified OPA instance,\nallowing you to track health changes over time.\n\nResults are paginated using cursor-based pagination.\n","operationId":"getInstanceStatusHistory","tags":["Status"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/StatusInstanceIdPath"},{"$ref":"#/components/parameters/StatusInstanceLimit"},{"$ref":"#/components/parameters/StatusCursor"}],"responses":{"200":{"description":"Instance status history retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OPAInstanceHistoryResponse"},"example":{"success":true,"data":[{"id":"550e8400-e29b-41d4-a716-446655440000","entity_id":"123e4567-e89b-12d3-a456-426614174000","entity_name":"Production API","pdp_instance_id":"1780d507-aea2-45cc-ae50-fa153c8e4a5a","pdp_version":"0.60.0","health_status":"healthy","plugins_healthy":true,"primary_bundle_revision":"ABC123","reported_at":"2024-01-15T10:30:00.000Z","received_at":"2024-01-15T10:30:01.000Z","created_at":"2024-01-15T10:30:01.000Z"}],"pagination":{"cursor":"","has_more":false}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"OPA instance not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"not_found","message":"OPA instance with ID '1780d507-aea2-45cc-ae50-fa153c8e4a5a' not found"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/monitoring/metrics":{"get":{"summary":"Query monitoring metrics","description":"Query time-series monitoring metrics for fleet health charts.\n\nReturns metric datapoints for the specified time range and metric type.\nSupports filtering by entity_id.\n\nAuthentication: JWT (Bearer token)\nAuthorization: OPA status:read permission\n","operationId":"getMonitoringMetrics","tags":["Monitoring"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"metric","in":"query","required":true,"description":"The metric to query","schema":{"type":"string","enum":["status_reports","bundle_activation_age","healthy_instances","degraded_instances","unhealthy_instances","unknown_instances","total_instances"]}},{"name":"start","in":"query","required":false,"description":"Start time (ISO 8601). Defaults to 1 hour ago.","schema":{"type":"string","format":"date-time"}},{"name":"end","in":"query","required":false,"description":"End time (ISO 8601). Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"period","in":"query","required":false,"description":"Aggregation period in seconds (default 300)","schema":{"type":"integer","minimum":60,"maximum":86400,"default":300}},{"name":"entity_id","in":"query","required":false,"description":"Filter by entity ID","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Metrics retrieved successfully","content":{"application/json":{"schema":{"type":"object","required":["success","data","time_range","period"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","required":["metric","datapoints","unit"],"properties":{"metric":{"type":"string"},"datapoints":{"type":"array","items":{"type":"object","required":["timestamp","value"],"properties":{"timestamp":{"type":"string","format":"date-time"},"value":{"type":"number"}}}},"unit":{"type":"string"}}},"time_range":{"$ref":"#/components/schemas/TimeRange"},"period":{"type":"integer","description":"Aggregation period in seconds"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/monitoring/errors":{"get":{"summary":"Query recent monitoring errors","description":"Query recent PDP status errors for troubleshooting.\n\nReturns recent degraded/unhealthy status entries for troubleshooting.\n\nAuthentication: JWT (Bearer token)\nAuthorization: OPA status:read permission\n","operationId":"getMonitoringErrors","tags":["Monitoring"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"start","in":"query","required":false,"description":"Start time (ISO 8601). Defaults to 24 hours ago.","schema":{"type":"string","format":"date-time"}},{"name":"end","in":"query","required":false,"description":"End time (ISO 8601). Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of errors to return (default 50, max 100)","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"entity_id","in":"query","required":false,"description":"Filter by entity ID","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Errors retrieved successfully","content":{"application/json":{"schema":{"type":"object","required":["success","data","time_range"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["timestamp","pdp_instance_id","health_status"],"properties":{"timestamp":{"type":"string","format":"date-time"},"pdp_instance_id":{"type":"string"},"entity_id":{"type":"string","format":"uuid"},"entity_name":{"type":"string"},"health_status":{"type":"string","enum":["degraded","unhealthy"]},"error_message":{"type":"string"}}}},"scanned_bytes":{"type":"integer"}}},"time_range":{"$ref":"#/components/schemas/TimeRange"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/ci-providers/{id}":{"get":{"summary":"Get CI provider","description":"Get a specific CI provider by ID","operationId":"getCIProvider","tags":["CI Providers"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/CIProviderId"}],"responses":{"200":{"description":"CI provider details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CIProviderResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"summary":"Update CI provider","description":"Update a CI provider's name, audience, or enabled status","operationId":"updateCIProvider","tags":["CI Providers"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/CIProviderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCIProviderRequest"}}}},"responses":{"200":{"description":"CI provider updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CIProviderResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete CI provider","description":"Delete a CI provider and all associated trust policies","operationId":"deleteCIProvider","tags":["CI Providers"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/CIProviderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"CI provider deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/integrations/github/install/start":{"get":{"summary":"Start GitHub App install flow","description":"Issues a signed, short-lived state token binding the current tenant and user, then\nreturns a `302` redirect to GitHub's App installation prompt\n(`https://{host}/apps/{slug}/installations/new?state=...`). Supports github.com and\nGitHub Enterprise Server via the optional `host` query parameter.\n\n**Do not follow this redirect programmatically.** The expected client behavior is to\nnavigate the user's browser to this URL (`window.location.assign(...)`) so they can\ncomplete the install in GitHub's UI. After they finish, GitHub will redirect to the\ncallback below.\n","operationId":"startGitHubAppInstall","tags":["Integrations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"host","in":"query","required":false,"schema":{"type":"string","maxLength":255},"description":"Git host to install on. Defaults to `github.com`. For GHES, pass the\ninstance hostname (e.g. `github.acme.corp`).\n"},{"name":"return_to","in":"query","required":false,"schema":{"type":"string","format":"uri","maxLength":2048},"description":"URL the callback should redirect to after a successful install. Validated\nserver-side against an allow-list; invalid values fall back to a safe default.\n"}],"responses":{"302":{"description":"Redirect to GitHub's App installation prompt. Clients **must not** follow this\nredirect programmatically — navigate the user's browser instead.\n","headers":{"Location":{"schema":{"type":"string","format":"uri"},"description":"Fully-qualified GitHub install URL with signed `state` query."}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/integrations/github/install/callback":{"get":{"summary":"GitHub App install callback","description":"GitHub redirects the user here after they install (or re-install) the EnforceAuth\nGitHub App. The endpoint is **public** — the signed `state` token (verified server-side)\nIS the tenant binding and acts as CSRF defense.\n\nOn success, fetches the installation metadata via the App JWT, upserts\n`tenant_github_integrations`, and returns a `302` to the validated `return_to` URL from\nthe original state token. This endpoint is exercised by GitHub's browser flow only —\nnot by API clients — so should not be followed programmatically.\n","operationId":"completeGitHubAppInstallCallback","tags":["Integrations"],"security":[],"parameters":[{"name":"installation_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64}},{"name":"setup_action","in":"query","required":false,"schema":{"type":"string","maxLength":64},"description":"GitHub's setup-action signal (typically `install`)."},{"name":"state","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":4096},"description":"Signed state token issued by `/install/start`."}],"responses":{"302":{"description":"Redirect to the validated `return_to` URL on success.","headers":{"Location":{"schema":{"type":"string","format":"uri"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"description":"GitHub installation lookup failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/integrations/github":{"get":{"summary":"List GitHub integrations","description":"List active (non-revoked) GitHub App installations for the current tenant. Returns\nidentifiers and installation context only — no secrets.\n","operationId":"listGitHubIntegrations","tags":["Integrations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"List of active GitHub integrations for the tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantGithubIntegrationListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/integrations/github/{id}":{"delete":{"summary":"Disconnect GitHub integration","description":"Soft-revoke a GitHub App installation (`revoked_at = now()`) and best-effort call\n`DELETE /app/installations/{id}` upstream to uninstall the App from the org. Idempotent:\nrevoking an already-revoked integration returns `204` and is a no-op.\n","operationId":"deleteGitHubIntegration","tags":["Integrations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Integration row ID."}],"responses":{"204":{"description":"Integration revoked (or already revoked — idempotent)."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/trust-policies":{"get":{"summary":"List trust policies","description":"List all trust policies for the tenant with optional filtering","operationId":"listTrustPolicies","tags":["Trust Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by entity ID"},{"name":"ci_provider_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by CI provider ID"}],"responses":{"200":{"description":"List of trust policies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrustPolicyListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Create trust policy","description":"Create a new trust policy that maps CI provider claims to EnforceAuth permissions.\nTrust policies define which CI tokens are accepted and what scopes they receive.\n","operationId":"createTrustPolicy","tags":["Trust Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrustPolicyRequest"}}}},"responses":{"201":{"description":"Trust policy created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrustPolicyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Policy with this name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/trust-policies/{id}":{"get":{"summary":"Get trust policy","description":"Get a specific trust policy by ID","operationId":"getTrustPolicy","tags":["Trust Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/TrustPolicyId"}],"responses":{"200":{"description":"Trust policy details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrustPolicyResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"summary":"Update trust policy","description":"Update a trust policy's patterns, scopes, or enabled status","operationId":"updateTrustPolicy","tags":["Trust Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/TrustPolicyId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTrustPolicyRequest"}}}},"responses":{"200":{"description":"Trust policy updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrustPolicyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete trust policy","description":"Delete a trust policy","operationId":"deleteTrustPolicy","tags":["Trust Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/TrustPolicyId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"Trust policy deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/me":{"get":{"summary":"Get current user","description":"Returns the current authenticated user's profile and organizations.\nOrganizations are loaded from the EA entity system, not from JWT claims.\nThis endpoint should be called after authentication to discover which\norganizations the user belongs to.\n","operationId":"getCurrentUser","tags":["Users"],"responses":{"200":{"description":"Current user info","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["user","organizations","currentTenantId"],"properties":{"user":{"type":"object","required":["id","email"],"properties":{"id":{"type":"string","format":"uuid","description":"User entity ID"},"email":{"type":"string","format":"email","description":"User email address"}}},"organizations":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid","description":"Organization entity ID"},"name":{"type":"string","description":"Organization name"}}}},"currentTenantId":{"type":"string","format":"uuid","description":"Currently active tenant ID"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities":{"get":{"summary":"List entities","description":"List all entities with optional filtering","operationId":"listEntities","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"entityType","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by entity type"},{"name":"parentId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by parent entity ID"},{"name":"rootOnly","in":"query","required":false,"schema":{"type":"boolean"},"description":"Return only root entities (no parents)"},{"name":"tree","in":"query","required":false,"schema":{"type":"boolean"},"description":"Return full hierarchy tree"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/EntityListResponse"},{"$ref":"#/components/schemas/EntityTreeResponse"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Create entity","description":"Create a new entity","operationId":"createEntity","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityCreate"}}}},"responses":{"201":{"description":"Entity created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/with-relation":{"post":{"summary":"Create entity with relation","description":"Create a new entity and establish a relationship atomically.\nUse this when you need to create an entity that should be related to another entity.\nThe relation direction determines which entity is the source/target.\n","operationId":"createEntityWithRelation","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityCreateWithRelation"}}}},"responses":{"201":{"description":"Entity created successfully with relation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityWithRelationResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Target entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}":{"get":{"summary":"Get entity","description":"Get a single entity by ID","operationId":"getEntity","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["relations"]},"description":"Include relations in response"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/EntityResponse"},{"$ref":"#/components/schemas/EntityWithRelationsResponse"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"put":{"summary":"Update entity","description":"Update an existing entity","operationId":"updateEntity","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityUpdate"}}}},"responses":{"200":{"description":"Entity updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete entity","description":"Delete an entity (cascades to relations)","operationId":"deleteEntity","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"204":{"description":"Entity deleted successfully"},"400":{"description":"Entity has children and cannot be deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/parents":{"get":{"summary":"Get entity parents","description":"Get direct parent entities","operationId":"getEntityParents","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/children":{"get":{"summary":"Get entity children","description":"Get direct child entities","operationId":"getEntityChildren","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/ancestors":{"get":{"summary":"Get entity ancestors","description":"Get all ancestor entities (supports multiple parents)","operationId":"getEntityAncestors","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/descendants":{"get":{"summary":"Get entity descendants","description":"Get all descendant entities","operationId":"getEntityDescendants","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/relations":{"get":{"summary":"Get entity relations","description":"Get all relations for an entity","operationId":"getEntityRelations","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelationListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Create relation","description":"Create a relation from this entity to another (with cycle detection)","operationId":"createRelation","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelationCreate"}}}},"responses":{"201":{"description":"Relation created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelationResponse"}}}},"400":{"description":"Invalid relation (cycle detected, leaf entity, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/relations/{relationId}":{"delete":{"summary":"Delete relation","description":"Delete a specific relation","operationId":"deleteRelation","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/RelationId"}],"responses":{"204":{"description":"Relation deleted successfully"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/users":{"get":{"summary":"List entity users","description":"List all users of an entity (org or system)","operationId":"listEntityUsers","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"List of entity users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityUserListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Add user to entity","description":"Add a user to an entity (org or system) with a specific role","operationId":"addUserToEntity","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUserToEntityRequest"}}}},"responses":{"201":{"description":"User added to entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityUserResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/users/invite":{"post":{"summary":"Invite user to entity","description":"Create an invitation for a user to join an entity (organization).\nFor new users, creates a pending user entity and sends an invitation email.\nFor existing users, adds them directly to the organization.\n","operationId":"inviteUserToEntity","tags":["Invitations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role"],"properties":{"email":{"type":"string","format":"email","description":"Email address to invite"},"role":{"type":"string","enum":["administrator","engineer"],"description":"Role to assign to the invited user"}}}}}},"responses":{"201":{"description":"Invitation created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"token":{"type":"string","description":"Invite token (only for new users)"},"isNewUser":{"type":"boolean","description":"Whether this is a new user"},"userId":{"type":"string","format":"uuid","description":"The user entity ID"},"relationId":{"type":"string","format":"uuid","description":"The membership relation ID"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/invitations":{"get":{"summary":"List pending invitations","description":"List all pending invitations for an entity","operationId":"listEntityInvitations","tags":["Invitations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"List of pending invitations","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","items":{"type":"object","properties":{"relationId":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"role":{"type":"string"},"invitedBy":{"type":"string","format":"uuid"},"invitedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"organizationId":{"type":"string","format":"uuid"},"organizationName":{"type":"string"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/invitations/{inviteId}":{"delete":{"summary":"Revoke invitation","description":"Revoke a pending invitation","operationId":"revokeInvitation","tags":["Invitations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"name":"inviteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The invitation ID to revoke"}],"responses":{"204":{"description":"Invitation revoked successfully"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/users/{userId}":{"patch":{"summary":"Update user role","description":"Update a user's role in an entity","operationId":"updateEntityUserRole","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/EntityUserId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEntityUserRoleRequest"}}}},"responses":{"200":{"description":"User role updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityUserResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Remove user from entity","description":"Remove a user from an entity","operationId":"removeUserFromEntity","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/EntityUserId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"User removed from entity"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/configs":{"get":{"summary":"List entity configs","description":"Get all configurations for an entity","operationId":"listEntityConfigs","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListConfigsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/configs/{type}":{"get":{"summary":"Get entity config","description":"Get a specific configuration for an entity.\n\nWith `?effective=true`, returns the effective config (after\ninheritance). The path-based `/configs/{type}/effective` endpoint is\npreferred for new code; this query-string variant remains for\nbackwards compatibility. EA-653: when both `effective=true` and `env`\nare provided AND `type` is `bundle_destination`, the env's partial\nbundle_destination override is applied on top of the inheritance\nresult (same semantics as `/configs/{type}/effective?env=…`).\n","operationId":"getEntityConfig","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/ConfigType"},{"name":"effective","in":"query","required":false,"description":"When `true`, return the effective config after inheritance.","schema":{"type":"string","enum":["true","false"]}},{"name":"env","in":"query","required":false,"description":"Optional environment name. Only meaningful when `effective=true`\nAND `type` is `bundle_destination`. (EA-653)\n","schema":{"type":"string","minLength":1,"maxLength":255}}],"responses":{"200":{"description":"Successful response. Shape depends on the `effective` query\nparameter: with `effective=true` (and especially when paired\nwith `env` for `bundle_destination`) the route forwards to\ngetEffectiveConfig and returns the EffectiveConfigResponse\nshape (effective_config + config_sources + validation);\notherwise it returns the raw GetConfigResponse shape.\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/GetConfigResponse"},{"$ref":"#/components/schemas/EffectiveConfigResponse"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"put":{"summary":"Set entity config","description":"Set or update a configuration for an entity","operationId":"setEntityConfig","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/ConfigType"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["config_data"],"properties":{"config_data":{"type":"object","additionalProperties":true,"description":"Config data object (schema depends on config type)"}}}}}},"responses":{"200":{"description":"Config updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetConfigResponse"}}}},"201":{"description":"Config created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetConfigResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete entity config","description":"Delete a configuration for an entity","operationId":"deleteEntityConfig","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/ConfigType"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"Config deleted successfully"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/configs/{type}/effective":{"get":{"summary":"Get effective config","description":"Get the effective configuration after inheritance from ancestors.\n\nEA-653: when `env` is provided AND `type` is `bundle_destination`, the\nnamed environment's partial `bundle_destination` override is applied\non top of the inheritance result. Mirrors the merge that the deploy\npipeline performs at deploy time, so callers (e.g. the deploy dialog\npreview) see exactly what will actually deploy.\n\nFor other config types, `env` is accepted but ignored (only\nbundle_destination has a meaningful per-env override today). If the\nnamed env is not defined for this entity, the base effective config\nis returned with an informational warning in `validation.warnings`.\n","operationId":"getEffectiveConfig","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/ConfigType"},{"name":"env","in":"query","required":false,"description":"Optional environment name. When set and `type` is\n`bundle_destination`, applies the matching environment's partial\noverride on top of the inherited bundle_destination. Ignored for\nother config types. (EA-653)\n","schema":{"type":"string","minLength":1,"maxLength":255}}],"responses":{"200":{"description":"Successful response with inherited config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EffectiveConfigResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/environments":{"get":{"summary":"Get entity environments","description":"Get environment configuration summary for an entity","operationId":"getEntityEnvironments","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["environments","default_environment","details"],"properties":{"environments":{"type":"array","items":{"type":"string"}},"default_environment":{"type":"string","minLength":1,"nullable":true},"details":{"type":"object","additionalProperties":{"type":"object","required":["auto_deploy"],"properties":{"branch":{"type":"string","minLength":1},"auto_deploy":{"type":"boolean"}}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/git-permissions":{"get":{"summary":"Check entity Git write permissions","description":"Checks whether the entity's PAT has write access to its Git repository.\nUsed by the frontend to enable/disable policy saves and show the\nappropriate permissions state in the Create Policy flow.\nOnly supports Git storage type with GitHub provider.\n","operationId":"getEntityGitPermissions","tags":["Entities"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Git permissions check result","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","required":["canWrite","provider"],"properties":{"canWrite":{"type":"boolean"},"provider":{"type":"string"},"reason":{"type":"string","nullable":true,"description":"Machine-readable explanation when canWrite is false (auth_error, permission_denied, not_found)"}}}}}}}},"400":{"description":"Invalid storage type (not git)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Entity or config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/policy-files":{"get":{"summary":"Get policy file tree","description":"Fetch the tree of policy files from the entity's configured policy source.\nUses the effective PolicySourceConfig (after inheritance) to determine the\nstorage backend. Supports Git, S3, GCS, and Azure storage types — the\nresponse `storage_type` field indicates which backend served the tree.\n","operationId":"getPolicyFileTree","tags":["Policy Files"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Policy file tree","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyFilesTreeResponse"}}}},"400":{"description":"Unsupported storage type for file browsing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Storage backend authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Entity or config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/policy-files/{path+}":{"get":{"summary":"Get policy file content","description":"Fetch the content of a specific policy file from the entity's configured\npolicy source. Uses the effective PolicySourceConfig (after inheritance)\nto determine the storage backend. Supports Git, S3, GCS, and Azure\nstorage types. Only .rego files are returned.\n","operationId":"getPolicyFileContent","tags":["Policy Files"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"name":"path","in":"path","required":true,"schema":{"type":"string"},"description":"File path within the configured policy source (must end with .rego)"}],"responses":{"200":{"description":"Policy file content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyFileContentResponse"}}}},"400":{"description":"Unsupported storage type or invalid file type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Storage backend authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Entity, config, or file not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/policy-index":{"get":{"summary":"Get policy index","description":"Get the build-time policy index for an entity. Maps .rego files to their\nOPA packages and rules. Used to trace decision logs back to source files.\n","operationId":"getPolicyIndex","tags":["Policy Index"],"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"name":"revision","in":"query","required":false,"schema":{"type":"string","maxLength":255},"description":"Bundle revision (git SHA). If omitted, returns the latest index."}],"responses":{"200":{"description":"Policy index","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyIndexResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Policy index not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/environments/{env}/drift":{"get":{"summary":"Get environment drift","description":"Compare policy content between the specified environment and its promotion target.\nReturns which modules were added, removed, or modified.\nThe target environment is resolved from the entity's environment_config `promotes_to` field.\n","operationId":"getEnvironmentDrift","tags":["Promotions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"name":"env","in":"path","required":true,"description":"Source environment name","schema":{"type":"string","pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$","maxLength":100}}],"responses":{"200":{"description":"Drift comparison result","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/DriftResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Entity or environment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/promotions":{"get":{"summary":"List promotion events","description":"List promotion events for an entity, ordered by most recent first.\nShows the audit trail of environment-to-environment promotions.\n","operationId":"listPromotions","tags":["Promotions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Promotion events list","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["promotions"],"properties":{"promotions":{"type":"array","items":{"$ref":"#/components/schemas/PromotionEvent"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Create promotion event","description":"Record a promotion event for an entity. Creates a pending promotion record\nand publishes an outbox event for downstream processing.\n","operationId":"createPromotion","tags":["Promotions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["source_environment","target_environment"],"properties":{"source_environment":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"target_environment":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"promotion_method":{"type":"string","enum":["console","api","cli","ci"],"default":"console"}}}}}},"responses":{"201":{"description":"Promotion event created","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/PromotionEvent"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/policies/deploy":{"post":{"summary":"Trigger policy deployment","description":"Manually trigger a policy deployment for an entity. This initiates the same pipeline\nthat webhook-triggered deployments use:\n\n1. A deployment run record is created (status: `pending`).\n2. The policy pipeline is triggered asynchronously.\n3. The pipeline fetches the effective `policy_source` config for the entity, clones the\n   policy repository, runs `opa test` / `eopa test`, compiles an OPA bundle, and uploads\n   the bundle to the configured destination.\n4. The deployment run transitions through phases: `init → fetch → test → build → deploy → complete`.\n5. On completion, the deployment status is updated to `success` or `failed`.\n\nUse `scope=descendants` to fan out deployment to all descendant entities.\nFan-out deployments share a `group_id` for tracking.\n\n## Restore (re-deploy a prior run)\n\nTo re-deploy a previously successful run as a recovery action, send a deploy request with:\n\n- `commit_sha` set to the commit SHA of the deployment run you want to restore.\n- `restore_of_run_id` set to the 16-character `run_id` of the run that introduced\n  the regression (i.e. the run being replaced). This links the new run to the source\n  run for audit/UI purposes; the new run still executes the full pipeline against\n  the target `commit_sha`.\n\nThis is the rebuild-from-SHA recovery path. For deterministic same-bytes\nrestore, use the artifact-based path: call `GET\n/v1/entities/{id}/deployments/{run_id}/restore-preview` to confirm the\nartifact is restorable, then submit this endpoint with `commit_sha` and\n`restore_of_run_id` set to the target run.\n\nExample restore request body:\n\n```json\n{\n  \"commit_sha\": \"abc123def4567890abc123def4567890abc12345\",\n  \"restore_of_run_id\": \"1a2b3c4d5e6f7a8b\"\n}\n```\n","operationId":"triggerPolicyDeploy","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"name":"scope","in":"query","required":false,"description":"Deploy scope. 'self' deploys to the target entity only (default).\n'descendants' fans out to all deployable descendant entities.\n","schema":{"type":"string","enum":["self","descendants"],"default":"self"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean","description":"Force deployment even if no changes detected","default":false},"commit_sha":{"type":"string","description":"Specific commit SHA to deploy (optional)"},"restore_of_run_id":{"type":"string","minLength":16,"maxLength":16,"description":"16-character `run_id` of the deployment run being replaced. When set,\nthis run is recorded as a restore of the referenced run; combine with\n`commit_sha` set to the prior good commit to re-deploy that state\n(rebuild-from-SHA path). For deterministic same-bytes restore,\nconfirm via `getRestorePreview` and submit this endpoint with\n`restore_of_run_id` plus the original `commit_sha`.\n"},"environment":{"type":"string","maxLength":100,"description":"Target environment for bundle promotion (EA-123)"}}}}}},"responses":{"202":{"description":"Deployment triggered","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","enum":[true]},"run_id":{"type":"string","description":"Unique identifier for this deployment run (scope=self)"},"group_id":{"type":"string","format":"uuid","description":"Shared group ID for org-level fan-out (scope=descendants)"},"triggered":{"type":"array","description":"Entities that were triggered (scope=descendants)","items":{"type":"object","properties":{"entity_id":{"type":"string"},"entity_name":{"type":"string"},"run_id":{"type":"string"}}}},"skipped":{"type":"array","description":"Entities that were skipped (scope=descendants)","items":{"type":"object","properties":{"entity_id":{"type":"string"},"entity_name":{"type":"string"},"reason":{"type":"string"}}}},"warnings":{"type":"array","description":"Warnings about entities with active deployments (scope=descendants)","items":{"type":"object","properties":{"entity_id":{"type":"string"},"entity_name":{"type":"string"},"message":{"type":"string"}}}},"message":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/flags":{"get":{"summary":"Evaluate feature flags for the authenticated caller","description":"Returns the evaluated product-flag map for the caller's\n(tenantId, userId).\n\nEvaluation precedence: user override > tenant override > global\ndefault.\n\nFail-closed: on internal error the endpoint still returns 200\nwith `{ flags: {} }` so the frontend's DEFAULT_FLAGS registry\nkicks in. Callers should treat this endpoint as advisory and\nalways have a local default.\n","operationId":"evaluateFeatureFlags","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Evaluated flag map","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagEvaluation"},"example":{"success":true,"data":{"flags":{"policy-create":{"enabled":true,"source":"tenant"},"ai-ui-assistant":{"enabled":false,"source":"default"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/public-flags":{"get":{"summary":"Get public marketing-scope feature flag map (UNAUTHENTICATED)","description":"Returns the marketing-scope flag map keyed by `flagKey`. No\ntenant context — the marketing site is anonymous. Defaults only;\noverrides are never applied at marketing scope.\n\nCached at the edge for 30 seconds. The only allowed `scope` is\n`marketing`; any other value returns 400.\n","operationId":"getPublicFeatureFlags","tags":["Feature Flags"],"security":[],"parameters":[{"name":"scope","in":"query","required":true,"schema":{"type":"string","enum":["marketing"]},"description":"Must be `marketing`. Other scopes require authentication."}],"responses":{"200":{"description":"Public marketing flag map","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"Edge cache directive","example":"public, max-age=30, s-maxage=30"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicFlagMap"},"example":{"success":true,"data":{"flags":{"web_page_home_visible":true,"web_page_our_team_visible":false}}}}}},"400":{"description":"Missing or invalid `scope` query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"invalid_scope","message":"Query parameter 'scope' must be 'marketing'"}}}}}}},"/v1/feature-flags/admin":{"get":{"summary":"List feature flag definitions","description":"Returns every flag definition (optionally filtered by `scope`)\nplus a per-flag `overrideCount` so the admin UI can render a\n\"N tenants override this\" badge without enumerating overrides.\n\nRequires the `feature_flag:read` OPA permission (granted to\n`administrator` and `engineer`).\n","operationId":"listFeatureFlagsAdmin","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"scope","in":"query","required":false,"schema":{"type":"string","enum":["product","marketing","admin"]},"description":"Optional scope filter"}],"responses":{"200":{"description":"Flag definitions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListFeatureFlagsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Create a feature flag definition","description":"Create a brand-new flag definition (global default). PM-admin\nonly — requires the `feature_flag:manage_global` OPA permission.\n","operationId":"createFeatureFlag","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlagRequest"},"example":{"flag_key":"policy-create","scope":"product","default_enabled":false,"description":"Gates Create Policy button/page (EA-411)"}}}},"responses":{"201":{"description":"Flag created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/feature-flags/admin/{flagKey}":{"patch":{"summary":"Update a feature flag definition","description":"Update the global default and/or description of a flag. At least\none of `default_enabled` or `description` must be provided.\nPM-admin only.\n","operationId":"updateFeatureFlag","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/FlagKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlagRequest"},"example":{"default_enabled":true}}}},"responses":{"200":{"description":"Flag updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete a feature flag definition","description":"Permanently delete a flag definition AND every override targeting\nit. PM-admin only. Prefer defaulting the flag to `false` instead\nof deleting in prod — deletes destroy audit history.\n","operationId":"deleteFeatureFlag","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/FlagKey"}],"responses":{"204":{"description":"Flag deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/feature-flags/admin/{flagKey}/overrides":{"get":{"summary":"List feature flag overrides for the caller's tenant","description":"Returns every override row applicable to (caller's tenantId,\nflagKey). Requires `feature_flag:read`.\n","operationId":"listFeatureFlagOverrides","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/FlagKey"}],"responses":{"200":{"description":"Overrides for the caller's tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListFeatureFlagOverridesResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/feature-flags/admin/{flagKey}/override":{"put":{"summary":"Upsert a feature flag override","description":"Set or update a tenant- or user-scoped override. For\n`scope_type='tenant'`, `scope_id` MUST equal the caller's\ntenantId — customer admins cannot manage other tenants'\noverrides.\n\nAuthorization depends on `scope_type`:\n- `tenant` → `feature_flag:manage_tenant`\n- `user`   → `feature_flag:manage_user`\n","operationId":"setFeatureFlagOverride","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/FlagKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOverrideRequest"},"example":{"scope_type":"tenant","scope_id":"098a0883-cfbf-4096-919a-da20c007e334","enabled":true,"reason":"Pilot customer for EA-411"}}}},"responses":{"200":{"description":"Override upserted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagOverrideResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Clear a feature flag override","description":"Delete a tenant- or user-scoped override. DELETE bodies have\nspotty proxy/runtime support, so the scope target is passed as\nquery parameters instead.\n","operationId":"clearFeatureFlagOverride","tags":["Feature Flags"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/FlagKey"},{"name":"scope_type","in":"query","required":true,"schema":{"type":"string","enum":["tenant","user"]},"description":"Override scope target type"},{"name":"scope_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Override scope target ID (tenantId or userId)"}],"responses":{"204":{"description":"Override cleared"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/configs/verify":{"post":{"summary":"Verify config","description":"Verify configuration connectivity (test git/S3 access)","operationId":"verifyConfig","tags":["Configs"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyConfigRequest"}}}},"responses":{"200":{"description":"Verification result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyConfigResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/credential-types":{"get":{"summary":"Get credential types","description":"Returns available credential type definitions for rendering dynamic secret forms.\nTypes include aws_s3, aws_iam_role, generic, gcs_service_account, and azure_blob with their field schemas.\nLabels, descriptions, and help text are defined in frontend i18n files.\n","operationId":"getCredentialTypes","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Credential types retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","items":{"type":"object","required":["type","fields"],"properties":{"type":{"type":"string","description":"Type identifier used for i18n lookup","example":"aws_s3"},"fields":{"type":"array","items":{"type":"object","required":["key","required","sensitive"],"properties":{"key":{"type":"string","description":"Field key used for i18n lookup and storage"},"required":{"type":"boolean"},"sensitive":{"type":"boolean"}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/secrets":{"get":{"summary":"List all secrets","description":"List all secrets (metadata only) across all entities in the tenant hierarchy.\nReturns secrets grouped by entity. This is more efficient than making individual\nrequests per entity as it avoids Lambda throttling issues.\n","operationId":"listAllSecrets","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Secrets retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllSecretsListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Store secret","description":"Store a new secret for the specified entity","operationId":"storeSecret","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreSecretRequest"},"example":{"entityType":"system","entityId":"550e8400-e29b-41d4-a716-446655440000","secretName":"git_token","secretValue":"ghp_token123"}}}},"responses":{"201":{"description":"Secret stored successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreSecretResponse"},"example":{"success":true,"data":{"id":"123e4567-e89b-12d3-a456-426614174000","locator":"arn:aws:secretsmanager:us-east-1:123456789012:secret:tenant/system/uuid/git_token","provider":"aws_secrets_manager"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/secrets/available/{entityId}":{"get":{"summary":"List available secrets for an entity","description":"List all secrets available to an entity, including secrets from ancestor entities\nin the hierarchy. Used by the frontend SecretPicker dropdown to show secrets that\ncan be referenced by a config on this entity.\n","operationId":"listAvailableSecrets","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"entityId","in":"path","required":true,"description":"Entity ID to list available secrets for","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Available secrets retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableSecretsListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/secrets/{entityType}/{entityId}/{secretName}":{"get":{"summary":"Get secret metadata","description":"Get metadata about a secret (does NOT return the secret value).\nUse this to verify a secret exists and get its provider/locator information.\n","operationId":"getSecretMetadata","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/SecretEntityType"},{"$ref":"#/components/parameters/SecretEntityId"},{"$ref":"#/components/parameters/SecretName"}],"responses":{"200":{"description":"Secret metadata retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretMetadataResponse"},"example":{"success":true,"data":{"id":"123e4567-e89b-12d3-a456-426614174000","tenantId":"tenant-uuid","secretName":"git_token","entityType":"system","entityId":"550e8400-e29b-41d4-a716-446655440000","provider":"aws_secrets_manager","locator":"arn:aws:secretsmanager:us-east-1:123456789012:secret:tenant/system/uuid/git_token","createdAt":"2025-01-01T00:00:00Z","updatedAt":"2025-01-01T00:00:00Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete secret","description":"Delete a secret and remove its metadata from the database","operationId":"deleteSecret","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/SecretEntityType"},{"$ref":"#/components/parameters/SecretEntityId"},{"$ref":"#/components/parameters/SecretName"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"Secret deleted successfully"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/secrets/{entityType}/{entityId}/{secretName}/rotate":{"post":{"summary":"Rotate secret","description":"Rotate (update) a secret's value with a new credential","operationId":"rotateSecret","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/SecretEntityType"},{"$ref":"#/components/parameters/SecretEntityId"},{"$ref":"#/components/parameters/SecretName"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateSecretRequest"},"example":{"secretValue":"ghp_newtoken456"}}}},"responses":{"200":{"description":"Secret rotated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreSecretResponse"},"example":{"success":true,"data":{"id":"123e4567-e89b-12d3-a456-426614174000","locator":"arn:aws:secretsmanager:us-east-1:123456789012:secret:tenant/system/uuid/git_token","provider":"aws_secrets_manager"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/logs":{"post":{"summary":"Ingest decision logs","description":"Ingest decision logs from OPA/EOPA agents.\n\n**Authentication:** API key only — pass `X-API-Key: <key>`. JWT (Authorization\nheader) is **not** accepted on this endpoint as of EA-606. Workload write\nscopes (`decisions:write`, `status:write`) are reserved for API-key auth so\nhuman users (including via MCP / OAuth flows) cannot impersonate workloads.\nThe API key must have `decisions:write` scope and be scoped to the system\nsending logs.\n\n**Data pipeline:** Accepted logs are processed asynchronously through a streaming pipeline\nand stored durably, partitioned by tenant and date for efficient querying. In production,\nlogs are stored with immutable write-once retention for compliance.\n\nReturns 202 Accepted as logs are processed asynchronously.\nThe response indicates how many logs were accepted for processing.\n","operationId":"ingestLogs","tags":["Decisions"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestDecisionLogsRequest"},"example":[{"decision_id":"abc-123","timestamp":"2025-11-30T10:00:00.000Z","path":"authz/allow","input":{"user":"alice","action":"read","resource":"document"},"result":true,"labels":{"id":"opa-agent-1","version":"0.60.0"},"bundles":{"main":{"revision":"abc123"}},"metrics":{"timer_rego_query_eval_ns":1500000}}]}}},"responses":{"202":{"description":"Decision logs accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestDecisionLogsResponse"},"example":{"success":true,"data":{"accepted":1,"failed":0}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid X-API-Key header"}}}},"402":{"description":"Usage cutoff - payment required to resume processing","content":{"application/json":{"schema":{"type":"object","required":["success","error","message"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","example":"usage_cutoff"},"message":{"type":"string","example":"Free tier usage limit exceeded. Add billing information to continue."},"data":{"type":"object","properties":{"is_cutoff":{"type":"boolean"},"cta":{"type":"object","properties":{"message":{"type":"string"},"url":{"type":"string"}}}}}}}}}},"403":{"description":"Forbidden - insufficient permissions (missing decisions:write scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"forbidden","message":"Missing decisions:write permission"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"summary":"Query decision logs","description":"Query decision logs with filtering and pagination.\n\nSupports filtering by:\n- Time range (timestamp_start, timestamp_end)\n- Policy path\n- Decision outcome (allow, deny, error, partial)\n- System ID (OPA instance identifier)\n- Decision ID (exact match)\n- Bundle revision (exact match)\n\nSupports sorting by:\n- timestamp (default)\n- decision_outcome\n- path\n- system_id\n\nResults are paginated using cursor-based pagination.\n","operationId":"queryLogs","tags":["Decisions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/DecisionLogCursor"},{"$ref":"#/components/parameters/DecisionLogLimit"},{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by EnforceAuth entity ID"},{"$ref":"#/components/parameters/DecisionLogTimestampStart"},{"$ref":"#/components/parameters/DecisionLogTimestampEnd"},{"$ref":"#/components/parameters/DecisionLogPath"},{"$ref":"#/components/parameters/DecisionOutcome"},{"$ref":"#/components/parameters/DecisionLogSystemId"},{"$ref":"#/components/parameters/DecisionLogDecisionId"},{"$ref":"#/components/parameters/DecisionLogBundleRevision"},{"name":"random_sample","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Return a random sample of matching logs"},{"$ref":"#/components/parameters/DecisionLogSortBy"},{"$ref":"#/components/parameters/DecisionLogSortOrder"}],"responses":{"200":{"description":"Decision logs retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionLogListResponse"},"example":{"success":true,"data":[{"id":"550e8400-e29b-41d4-a716-446655440000","tenant_id":"098a0883-cfbf-4096-919a-da20c007e334","entity_id":"ent_01HXYZ","decision_id":"abc-123","timestamp":"2025-11-30T10:00:00.000Z","path":"authz/allow","input":{"user":"alice","action":"read","resource":"document"},"decision_outcome":"allow","system_id":"opa-agent-1","system_version":"0.60.0","bundle_revision":"abc123","evaluation_duration_ns":1500000,"received_at":"2025-11-30T10:00:01.000Z"}],"pagination":{"has_more":false}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid Authorization header"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/decisions/logs":{"post":{"summary":"Ingest decision logs (deprecated)","deprecated":true,"description":"Ingest decision logs from OPA/EOPA agents.\n\n**Authentication:** API key only — pass `X-API-Key: <key>`. JWT is\nnot accepted on this endpoint as of EA-606. The API key must have\n`decisions:write` scope and be scoped to the system sending logs.\nPrefer the canonical `/v1/logs` path; this alias remains for\nbackward compatibility.\n\n**Data pipeline:** Accepted logs are processed asynchronously through a streaming pipeline\nand stored durably, partitioned by tenant and date for efficient querying. In production,\nlogs are stored with immutable write-once retention for compliance.\n\nReturns 202 Accepted as logs are processed asynchronously.\nThe response indicates how many logs were accepted for processing.\n","operationId":"ingestDecisionLogs","tags":["Decisions"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestDecisionLogsRequest"},"example":[{"decision_id":"abc-123","timestamp":"2025-11-30T10:00:00.000Z","path":"authz/allow","input":{"user":"alice","action":"read","resource":"document"},"result":true,"labels":{"id":"opa-agent-1","version":"0.60.0"},"bundles":{"main":{"revision":"abc123"}},"metrics":{"timer_rego_query_eval_ns":1500000}}]}}},"responses":{"202":{"description":"Decision logs accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestDecisionLogsResponse"},"example":{"success":true,"data":{"accepted":1,"failed":0}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid X-API-Key header"}}}},"402":{"description":"Usage cutoff - payment required to resume processing","content":{"application/json":{"schema":{"type":"object","required":["success","error","message"],"properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","example":"usage_cutoff"},"message":{"type":"string","example":"Free tier usage limit exceeded. Add billing information to continue."},"data":{"type":"object","properties":{"is_cutoff":{"type":"boolean"},"cta":{"type":"object","properties":{"message":{"type":"string"},"url":{"type":"string"}}}}}}}}}},"403":{"description":"Forbidden - insufficient permissions (missing decisions:write scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"forbidden","message":"Missing decisions:write permission"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"summary":"Query decision logs","description":"Query decision logs with filtering and pagination.\n\nSupports filtering by:\n- Time range (timestamp_start, timestamp_end)\n- Policy path\n- Decision outcome (allow, deny, error, partial)\n- System ID (OPA instance identifier)\n- Decision ID (exact match)\n- Bundle revision (exact match)\n\nSupports sorting by:\n- timestamp (default)\n- decision_outcome\n- path\n- system_id\n\nResults are paginated using cursor-based pagination.\n","deprecated":true,"operationId":"queryDecisionLogs","tags":["Decisions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/DecisionLogCursor"},{"$ref":"#/components/parameters/DecisionLogLimit"},{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by EnforceAuth entity ID"},{"$ref":"#/components/parameters/DecisionLogTimestampStart"},{"$ref":"#/components/parameters/DecisionLogTimestampEnd"},{"$ref":"#/components/parameters/DecisionLogPath"},{"$ref":"#/components/parameters/DecisionOutcome"},{"$ref":"#/components/parameters/DecisionLogSystemId"},{"$ref":"#/components/parameters/DecisionLogDecisionId"},{"$ref":"#/components/parameters/DecisionLogBundleRevision"},{"name":"random_sample","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Return a random sample of matching logs"},{"$ref":"#/components/parameters/DecisionLogSortBy"},{"$ref":"#/components/parameters/DecisionLogSortOrder"}],"responses":{"200":{"description":"Decision logs retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionLogListResponse"},"example":{"success":true,"data":[{"id":"550e8400-e29b-41d4-a716-446655440000","tenant_id":"098a0883-cfbf-4096-919a-da20c007e334","entity_id":"ent_01HXYZ","decision_id":"abc-123","timestamp":"2025-11-30T10:00:00.000Z","path":"authz/allow","input":{"user":"alice","action":"read","resource":"document"},"decision_outcome":"allow","system_id":"opa-agent-1","system_version":"0.60.0","bundle_revision":"abc123","evaluation_duration_ns":1500000,"received_at":"2025-11-30T10:00:01.000Z"}],"pagination":{"has_more":false}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid Authorization header"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/logs/{id}":{"get":{"summary":"Get decision log by ID","description":"Retrieve a single decision log by its ID","operationId":"getLog","tags":["Decisions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/DecisionLogId"},{"name":"date","in":"query","required":false,"description":"Partition date (YYYY-MM-DD) for efficient Athena partition pruning. When provided, narrows the scan to a single date partition.","schema":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-02-17"}}],"responses":{"200":{"description":"Decision log retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionLogResponse"},"example":{"success":true,"data":{"id":"550e8400-e29b-41d4-a716-446655440000","tenant_id":"098a0883-cfbf-4096-919a-da20c007e334","decision_id":"abc-123","timestamp":"2025-11-30T10:00:00.000Z","path":"authz/allow","input":{"user":"alice","action":"read","resource":"document"},"result":true,"decision_outcome":"allow","system_id":"opa-agent-1","system_version":"0.60.0","bundle_revision":"abc123","evaluation_duration_ns":1500000,"received_at":"2025-11-30T10:00:01.000Z","partition_date":"2025-11-30"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid Authorization header"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Decision log not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"not_found","message":"Decision log with ID '550e8400-e29b-41d4-a716-446655440000' not found"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/decisions/logs/{id}":{"get":{"summary":"Get decision log by ID (deprecated)","deprecated":true,"description":"**Deprecated:** Use GET /v1/logs/{id} instead.\n\nRetrieve a single decision log by its ID\n","operationId":"getDecisionLog","tags":["Decisions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/DecisionLogId"},{"name":"date","in":"query","required":false,"description":"Partition date (YYYY-MM-DD) for efficient Athena partition pruning. When provided, narrows the scan to a single date partition.","schema":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-02-17"}}],"responses":{"200":{"description":"Decision log retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionLogResponse"},"example":{"success":true,"data":{"id":"550e8400-e29b-41d4-a716-446655440000","tenant_id":"098a0883-cfbf-4096-919a-da20c007e334","decision_id":"abc-123","timestamp":"2025-11-30T10:00:00.000Z","path":"authz/allow","input":{"user":"alice","action":"read","resource":"document"},"result":true,"decision_outcome":"allow","system_id":"opa-agent-1","system_version":"0.60.0","bundle_revision":"abc123","evaluation_duration_ns":1500000,"received_at":"2025-11-30T10:00:01.000Z","partition_date":"2025-11-30"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Unauthorized - missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"unauthorized","message":"Missing or invalid Authorization header"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Decision log not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"not_found","message":"Decision log with ID '550e8400-e29b-41d4-a716-446655440000' not found"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/deployments":{"get":{"summary":"List deployments","description":"List all deployments with optional filtering","operationId":"listDeployments","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/DeploymentEntityId"},{"$ref":"#/components/parameters/DeploymentStatus"},{"name":"group_id","in":"query","required":false,"description":"Filter by org-level fan-out group ID","schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/DeploymentSearch"},{"name":"sort_by","in":"query","required":false,"description":"Field to sort results by","schema":{"type":"string","enum":["started_at","completed_at","status","duration_ms","entity_name"],"default":"started_at"}},{"name":"sort_dir","in":"query","required":false,"description":"Sort direction","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"$ref":"#/components/parameters/DeploymentLimit"},{"$ref":"#/components/parameters/DeploymentOffset"}],"responses":{"200":{"description":"Deployments retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/deployments/stats":{"get":{"summary":"Get deployment stats","description":"Get aggregate deployment counts by status","operationId":"getDeploymentStats","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Deployment stats retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentStatsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/deployments/running":{"get":{"summary":"Get running deployments","description":"Get all currently running deployments","operationId":"getRunningDeployments","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/DeploymentEntityId"}],"responses":{"200":{"description":"Running deployments retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunningDeploymentsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/deployments/{run_id}":{"get":{"summary":"Get deployment by run ID","description":"Retrieve a specific deployment by its run ID","operationId":"getDeployment","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/RunId"}],"responses":{"200":{"description":"Deployment retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Delete deployment by run ID","description":"Delete a specific deployment run by its run ID.\n","operationId":"deleteDeployment","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/RunId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"Deployment deleted successfully"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/deployments":{"get":{"summary":"List entity deployments","description":"List all deployments for a specific entity","operationId":"listEntityDeployments","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/DeploymentStatus"},{"$ref":"#/components/parameters/DeploymentIncludeDescendants"},{"$ref":"#/components/parameters/DeploymentLimit"},{"$ref":"#/components/parameters/DeploymentOffset"}],"responses":{"200":{"description":"Entity deployments retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/deployments/latest":{"get":{"summary":"Get latest entity deployment","description":"Get the most recent deployment for a specific entity","operationId":"getLatestEntityDeployment","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"Latest deployment retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/deployments/{run_id}/restore-preview":{"get":{"summary":"Get restore preview for a deployment run","description":"Returns the target deployment run + its bundle artifact, the entity's\ncurrently-active artifact, a high-level diff summary, and the restore\neligibility decision (EA-671). Powers the Restore confirmation dialog.\n","operationId":"getRestorePreview","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/RunId"}],"responses":{"200":{"description":"Restore preview computed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestorePreviewResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/deployments/restore":{"post":{"summary":"Submit a policy bundle restore","description":"Submits a restore for a prior successful deployment (EA-671). The\nserver re-evaluates eligibility (so a stale or forged client\ncannot bypass the gate), creates a new `deployment_run` row in\n`pending` state, and writes a `deployment_run.restore_requested`\noutbox event in the same transaction. A downstream consumer\nperforms the storage copy (artifact mode) or rebuild\n(rebuild_from_sha mode) and flips the entity's active bundle\npointer.\n\nThe `acknowledgement` field MUST equal the literal string\n`RESTORE` (case-sensitive, no trim) — the dialog renders a\ntyped-confirmation input to satisfy this gate.\n\nCallers SHOULD send an `Idempotency-Key` header so a\ndouble-submitted dialog replays the prior 201 instead of\ncreating a second pending run.\n","operationId":"triggerRestore","tags":["Deployments"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerRestoreRequest"}}}},"responses":{"201":{"description":"Restore submitted; new deployment run created in pending state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerRestoreResponse"}}}},"400":{"description":"Acknowledgement mismatch or validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Caller lacks policies:deploy on this entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Source run not restorable, already active, mode mismatch, or\nunrecoverable. Body `error` discriminates among\n`not_restorable`, `already_active`, `mode_mismatch`,\n`unrecoverable`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/policy-logs":{"get":{"summary":"Query policy pipeline logs","description":"Query policy deployment pipeline logs for a specific entity","operationId":"queryPolicyLogs","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/PolicyLogRunId"},{"$ref":"#/components/parameters/PolicyLogLimit"},{"$ref":"#/components/parameters/PolicyLogStartTime"},{"$ref":"#/components/parameters/PolicyLogEndTime"}],"responses":{"200":{"description":"Policy logs retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyLogListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/secrets/{entityType}/{entityId}":{"get":{"summary":"List secrets for entity","description":"List all secrets (metadata only) for a specific entity","operationId":"listEntitySecrets","tags":["Secrets"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/SecretEntityType"},{"$ref":"#/components/parameters/SecretEntityId"}],"responses":{"200":{"description":"Secrets retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/internal/v1/entities/{id}/policy-index":{"post":{"summary":"Upload policy index (internal)","description":"Internal endpoint for the policy pipeline to upload build-time policy indexes.\nCalled after bundle build with Sigv4 (IAM) authentication.\n","operationId":"postPolicyIndexInternal","tags":["Internal"],"security":[{"AWS_IAM":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Entity identifier"},{"name":"X-Tenant-ID","in":"header","required":true,"schema":{"type":"string","format":"uuid"},"description":"Tenant identifier for defense-in-depth validation"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyIndexBody"}}}},"responses":{"201":{"description":"Policy index created/updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyIndexResponse"}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Entity not found or tenant mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/entities/{id}/api-keys":{"post":{"summary":"Create API key for entity","description":"Create a new API key scoped to an entity (org or system).\nThe full key is returned ONLY in this response - save it securely.\nKeys created for an org can write decision logs for all child systems.\n","operationId":"createApiKey","tags":["API Keys"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}}},"responses":{"201":{"description":"API key created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"summary":"List API keys for entity","description":"List all API keys scoped to a specific entity","operationId":"listEntityApiKeys","tags":["API Keys"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/EntityId"}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/api-keys":{"get":{"summary":"List all API keys for tenant","description":"List all API keys across all entities for the current tenant","operationId":"listApiKeys","tags":["API Keys"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Cursor for pagination"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum number of keys to return"}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/api-keys/{id}":{"get":{"summary":"Get API key details","description":"Get details of a specific API key (does not return the full key)","operationId":"getApiKey","tags":["API Keys"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/ApiKeyId"}],"responses":{"200":{"description":"API key details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"summary":"Update API key metadata","description":"Update name, description, or expiration of an API key","operationId":"updateApiKey","tags":["API Keys"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/ApiKeyId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApiKeyRequest"}}}},"responses":{"200":{"description":"API key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Revoke API key","description":"Permanently revoke an API key. This action cannot be undone.\nThe key will immediately stop working for authentication.\n","operationId":"revokeApiKey","tags":["API Keys"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/ApiKeyId"}],"responses":{"200":{"description":"API key revoked","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/permissions/me":{"get":{"summary":"Get effective permissions for current user","description":"Returns all effective permissions for the authenticated user based on their roles","operationId":"getMyPermissions","tags":["Permissions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"User's effective permissions","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","required":["userId","tenantId","roles","permissions"],"properties":{"userId":{"type":"string","description":"User ID"},"tenantId":{"type":"string","format":"uuid","description":"Current tenant context"},"roles":{"type":"array","items":{"type":"string"},"description":"Roles assigned to the user"},"permissions":{"type":"object","description":"Effective permissions, keyed by resource type. Each value\nis an array of allowed action strings (e.g.,\n`{entity: ['read', 'update'], policy: ['read']}`).\n","additionalProperties":{"type":"array","items":{"type":"string"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/permissions/check":{"post":{"summary":"Check permission for current user","description":"Check if the current user has a specific permission (dry-run mode)","operationId":"checkPermission","tags":["Permissions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resourceType","action"],"properties":{"resourceType":{"type":"string","description":"Type of resource (entity, config, secret, user, policy, audit)"},"action":{"type":"string","description":"Action to check (read, create, update, delete, etc.)"},"resourceId":{"type":"string","format":"uuid","description":"Optional specific resource ID"}}}}}},"responses":{"200":{"description":"Permission check result","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"allowed":{"type":"boolean"},"reason":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/permissions/roles":{"get":{"summary":"Get available role definitions","description":"Returns all available roles and their associated permissions","operationId":"getRoleDefinitions","tags":["Permissions"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Available role definitions","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean"},"data":{"type":"array","description":"Array of role definitions. The backend returns the array\ndirectly (no `{roles: [...]}` wrapper) — `reply.success(arr)`\nyields `{success: true, data: arr}`.\n","items":{"type":"object","required":["name","description","permissions"],"properties":{"name":{"type":"string","description":"Role name (e.g., 'administrator', 'engineer')"},"description":{"type":"string","description":"Human-readable description of the role"},"permissions":{"type":"object","description":"Permissions granted by this role, keyed by resource\ntype. Each value is an array of allowed action\nstrings.\n","additionalProperties":{"type":"array","items":{"type":"string"}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/audit/logs":{"get":{"summary":"Query audit logs","description":"Query audit logs with optional filters for date range, user, action, and resource type","operationId":"queryAuditLogs","tags":["Audit"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"start_date","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter logs after this date"},{"name":"end_date","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter logs before this date"},{"name":"user_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by user ID"},{"name":"action","in":"query","schema":{"type":"string"},"description":"Filter by action/event type (ILIKE pattern). Composes with event_types."},{"name":"event_types","in":"query","style":"form","explode":true,"schema":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":100}},"description":"Exact-match list of event_type values (EA-901). Canonical wire\nshape is repeated `event_types=` params; the backend also accepts\na single comma-separated string for back-compat. When set, the\nquery applies WHERE event_type IN (...). Composes with `action`.\n"},{"name":"resource_type","in":"query","schema":{"type":"string"},"description":"Filter by resource type (aggregateType)"},{"name":"outcome","in":"query","schema":{"type":"string","enum":["allowed","denied"]},"description":"Filter by authorization outcome"},{"name":"sort_dir","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Sort direction for the `createdAt` column (EA-901). Server-side\nso paginated callers see a consistent ordering across pages —\nclient-side sort would only reorder the visible page.\n"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"Page number for pagination"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":50},"description":"Number of results per page"}],"responses":{"200":{"description":"Audit logs","content":{"application/json":{"schema":{"type":"object","required":["success","data","pagination"],"properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"type":"object","required":["id","timestamp","eventType","userId","tenantId","action"],"properties":{"id":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"eventType":{"type":"string"},"userId":{"type":"string"},"tenantId":{"type":"string","format":"uuid"},"action":{"type":"string"},"resourceType":{"type":"string"},"resourceId":{"type":"string"},"allowed":{"type":"boolean"},"reason":{"type":"string"},"metadata":{"type":"object"}}}},"pagination":{"type":"object","required":["page","limit","total","hasMore"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasMore":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/audit/categories":{"get":{"summary":"List audit log event-type categories","description":"Returns the canonical mapping of category keys (e.g. `user_management`)\nto the `event_type` values that belong to each category. Used by audit\nUI surfaces (System Settings → Audit Trail tab) to populate filters\nwithout hardcoding the list on the client.\n","operationId":"listAuditCategories","tags":["Audit"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Audit categories","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","required":["key","eventTypes"],"properties":{"key":{"type":"string","description":"Stable identifier the UI uses to look up the localized label"},"eventTypes":{"type":"array","items":{"type":"string"},"description":"Exact-match list of event_type values in this category"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/audit/export":{"get":{"summary":"Export audit logs","description":"Export audit logs as JSON or CSV file","operationId":"exportAuditLogs","tags":["Audit"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"],"default":"json"},"description":"Export format"},{"name":"start_date","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter logs after this date"},{"name":"end_date","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter logs before this date"},{"name":"user_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by user ID"},{"name":"action","in":"query","schema":{"type":"string"},"description":"Filter by action/event type (ILIKE pattern). Composes with event_types."},{"name":"event_types","in":"query","style":"form","explode":true,"schema":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":100}},"description":"Exact-match list of event_type values (EA-901). Canonical wire\nshape is repeated `event_types=` params; the backend also accepts\na single comma-separated string for back-compat.\n"},{"name":"resource_type","in":"query","schema":{"type":"string"},"description":"Filter by resource type (aggregateType)"},{"name":"outcome","in":"query","schema":{"type":"string","enum":["allowed","denied"]},"description":"Filter by authorization outcome"}],"responses":{"200":{"description":"Exported audit logs file","headers":{"Content-Disposition":{"schema":{"type":"string"},"description":"Attachment filename"}},"content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/tiers":{"get":{"summary":"List pricing tiers","description":"Returns all active pricing tiers ordered by display_order.\nThis is global reference data shared across all tenants.\n","operationId":"listPricingTiers","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"List of pricing tiers","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/PricingTier"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/profile":{"get":{"summary":"Get tenant billing profile","description":"Returns the active billing profile for the authenticated tenant,\nincluding the associated pricing tier details.\nReturns null if no billing profile has been assigned.\n","operationId":"getTenantBillingProfile","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Billing profile (or null if none assigned)","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/TenantBillingProfile","nullable":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Assign pricing tier to tenant","description":"Creates a billing profile linking the tenant to a pricing tier.\nSupports optional custom overrides for enterprise contracts.\nReturns 409 if the tenant already has an active billing profile.\n","operationId":"assignTierToTenant","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignTierRequest"}}}},"responses":{"201":{"description":"Billing profile created","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/TenantBillingProfile"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Tenant already has an active billing profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/usage":{"get":{"summary":"List monthly usage summaries","description":"Returns monthly usage summaries for the authenticated tenant,\nordered by billing month descending (most recent first).\nSupports filtering by billing month and limiting results.\n","operationId":"getMonthlyUsage","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"billing_month","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"description":"Filter by billing month (YYYY-MM format)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":24,"default":12},"description":"Maximum number of months to return (default 12)"}],"responses":{"200":{"description":"List of monthly usage summaries","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/MonthlyUsageSummary"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/usage/current":{"get":{"summary":"Get current month usage","description":"Returns the usage summary for the current billing month.\nReturns null if no reconciliation has run yet for the current month.\n","operationId":"getCurrentMonthUsage","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Current month usage (or null if not yet reconciled)","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/MonthlyUsageSummary","nullable":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/usage/lifetime":{"get":{"summary":"Get lifetime usage total","description":"Returns the lifetime usage total for the authenticated tenant.\nAggregates all recorded monthly usage into a single total.\n","operationId":"getLifetimeUsage","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Lifetime usage total for the tenant","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["tenant_id","lifetime_decisions"],"properties":{"tenant_id":{"type":"string","format":"uuid","description":"The tenant this lifetime usage belongs to"},"lifetime_decisions":{"type":"integer","format":"int64","description":"Total number of policy decisions recorded across all months"},"earliest_month":{"type":"string","nullable":true,"description":"YYYY-MM of first recorded month"},"latest_month":{"type":"string","nullable":true,"description":"YYYY-MM of most recent recorded month"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/usage/cutoff-status":{"get":{"summary":"Get usage cutoff status","description":"Returns whether the tenant is currently cut off from processing\ndecision logs due to exceeding the free tier limit without billing\ninformation on file. Includes a CTA for the frontend banner.\n","operationId":"getUsageCutoffStatus","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Usage cutoff status for the tenant","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["is_cutoff"],"properties":{"is_cutoff":{"type":"boolean","description":"Whether the tenant is currently cut off from decision processing"},"reason":{"type":"string","nullable":true,"description":"Human-readable reason for the cutoff (null if not cut off)"},"cutoff_date":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 timestamp when the cutoff was applied"},"billing_month":{"type":"string","nullable":true,"description":"YYYY-MM of the billing month that triggered cutoff"},"total_decisions":{"type":"integer","format":"int64","nullable":true},"cta":{"type":"object","nullable":true,"properties":{"message":{"type":"string"},"url":{"type":"string"}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/billing/payment-method":{"get":{"summary":"Get active payment method","description":"Returns the active payment method for the authenticated tenant.\nReturns null data if no active payment method exists.\n","operationId":"getPaymentMethod","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Active payment method (or null if none)","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for the payment method"},"tenant_id":{"type":"string","format":"uuid","description":"Tenant that owns this payment method"},"payment_type":{"type":"string","description":"Type of payment method on file","enum":["credit_card","invoice","ach"]},"status":{"type":"string","description":"Current status of the payment method (e.g., active, removed)"},"last_four":{"type":"string","nullable":true,"description":"Last four digits of the card number (credit_card only)"},"expiry_month":{"type":"integer","nullable":true,"description":"Card expiration month (1-12, credit_card only)"},"expiry_year":{"type":"integer","nullable":true,"description":"Card expiration year (credit_card only)"},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when this payment method was added"},"updated_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of the last update"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"summary":"Add payment method","description":"Adds a payment method for the authenticated tenant.\nIf the tenant is currently suspended due to free tier cutoff,\nadding a payment method triggers automatic reactivation.\nSupports idempotency via Idempotency-Key header.\n","operationId":"addPaymentMethod","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["payment_type"],"properties":{"payment_type":{"type":"string","description":"Type of payment method to add","enum":["credit_card","invoice","ach"]},"last_four":{"type":"string","pattern":"^\\d{4}$","description":"Last four digits of card number (required for credit_card)"},"expiry_month":{"type":"integer","minimum":1,"maximum":12,"description":"Card expiration month (required for credit_card)"},"expiry_year":{"type":"integer","minimum":2024,"maximum":2100,"description":"Card expiration year (required for credit_card)"}}}}}},"responses":{"201":{"description":"Payment method created","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for the payment method"},"tenant_id":{"type":"string","format":"uuid","description":"Tenant that owns this payment method"},"payment_type":{"type":"string","description":"Type of payment method on file"},"status":{"type":"string","description":"Current status of the payment method (e.g., active)"},"last_four":{"type":"string","nullable":true,"description":"Last four digits of the card number (credit_card only)"},"expiry_month":{"type":"integer","nullable":true,"description":"Card expiration month (1-12, credit_card only)"},"expiry_year":{"type":"integer","nullable":true,"description":"Card expiration year (credit_card only)"},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when this payment method was added"},"updated_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of the last update"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"summary":"Remove payment method","description":"Removes the active payment method for the authenticated tenant.\nSets the payment method status to 'removed'.\nSupports idempotency via Idempotency-Key header.\n","operationId":"removePaymentMethod","tags":["Billing"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"204":{"description":"Payment method removed"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies/stats":{"get":{"summary":"Get policy statistics","description":"Returns aggregated policy counts by status and the fraction of policies\nthat are active.\n","operationId":"getPolicyStats","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Policy statistics","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["active","error","draft","disabled","total","activeFraction","linkedCount","linkedFraction","environments"],"properties":{"active":{"type":"integer","minimum":0},"error":{"type":"integer","minimum":0},"draft":{"type":"integer","minimum":0},"disabled":{"type":"integer","minimum":0},"total":{"type":"integer","minimum":0},"activeFraction":{"type":"number","minimum":0,"maximum":1},"linkedCount":{"type":"integer","minimum":0,"description":"Number of policies linked to at least one decision log"},"linkedFraction":{"type":"number","minimum":0,"maximum":1,"description":"Fraction of policies that are linked (0-1)"},"environments":{"type":"array","items":{"type":"string"},"description":"Distinct environment names configured across the tenant's entities (EA-508). Empty array if no environments are configured."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies/top-triggered":{"get":{"summary":"Get top triggered policies","description":"Returns the most frequently evaluated policies in a time range.","operationId":"getTopTriggeredPolicies","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"range","in":"query","schema":{"type":"string","enum":["7d","30d"],"default":"30d"}}],"responses":{"200":{"description":"Top triggered policies","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies/usage-over-time":{"get":{"summary":"Get policy usage over time","description":"Returns policy evaluation counts bucketed by day.","operationId":"getPolicyUsageOverTime","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"range","in":"query","schema":{"type":"string","enum":["24h","7d","30d"],"default":"30d"}},{"name":"date","in":"query","required":false,"description":"Date for 24h range (YYYY-MM-DD). Defaults to today. Ignored for 7d/30d.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"responses":{"200":{"description":"Policy usage time series","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies":{"post":{"summary":"Create a new policy","description":"Create a new policy by committing a .rego file to the entity's Git repository\non a new branch and opening a pull request.\nRequires policies:create authorization.\nSupports idempotency via the Idempotency-Key header.\n","operationId":"createPolicy","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Idempotency key for safe retries"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","entityId","code"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Policy name"},"entityId":{"type":"string","format":"uuid","description":"Target entity ID"},"code":{"type":"string","minLength":1,"description":"Rego policy code"},"description":{"type":"string","maxLength":2000,"description":"Optional policy description"},"filePath":{"type":"string","maxLength":500,"pattern":"^(?![/\\\\])(?![A-Za-z]:)(?!.*\\.\\.).+\\.rego$","description":"Optional file path override for the .rego file. When editing an existing policy, pass the original file path to prevent renaming. Must end with .rego, be relative (no leading /), and not contain path traversal (..). Null-byte and policy_path boundary checks are enforced server-side.\n"}}}}}},"responses":{"201":{"description":"Policy created — PR opened","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["policyName","entityId","filePath","branch","prNumber","prUrl","commitSha","status"],"properties":{"policyName":{"type":"string"},"entityId":{"type":"string","format":"uuid"},"filePath":{"type":"string"},"branch":{"type":"string"},"prNumber":{"type":"integer"},"prUrl":{"type":"string","format":"uri"},"commitSha":{"type":"string"},"status":{"type":"string","enum":["pr_opened"]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Entity or config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"summary":"List policies","description":"List policies with filtering, sorting, and pagination.\nPolicies are derived from policy indexes joined with entity and deployment data.\n","operationId":"listPolicies","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":255},"description":"Filter by fileName or package (case-insensitive)"},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","error","draft","disabled"]},"description":"Filter by policy status"},{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Filter by entity ID"},{"name":"environment","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"description":"Filter by environment name (EA-508). Only returns policies from policy indexes tagged with this environment."},{"name":"linked","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]},"description":"Filter by decision log linkage status"},{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","enum":["fileName","createdAt","lastEditedAt","lastInvokedAt"],"default":"fileName"},"description":"Field to sort by"},{"name":"sort_dir","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"Page number for pagination"},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":10},"description":"Number of results per page"}],"responses":{"200":{"description":"Paginated list of policies","content":{"application/json":{"schema":{"type":"object","required":["success","data","pagination"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"type":"object","required":["id","fileName","package","status","source","entityId","entityName","inheritedByCount","imposedOnCount","createdAt","lastEditedAt","lastInvokedAt","linked","lastEvaluatedAt","evaluationCount"],"properties":{"id":{"type":"string"},"fileName":{"type":"string"},"package":{"type":"string"},"status":{"type":"string","enum":["active","error","draft","disabled"]},"source":{"type":"string","enum":["indexed","non_indexed"],"description":"Whether this policy was discovered from a deployed index or from an unindexed source"},"entityId":{"type":"string","format":"uuid"},"entityName":{"type":"string"},"inheritedByCount":{"type":"integer","minimum":0},"imposedOnCount":{"type":"integer","minimum":0},"createdAt":{"type":"string","format":"date-time"},"lastEditedAt":{"type":"string","format":"date-time"},"lastInvokedAt":{"type":"string","format":"date-time","nullable":true},"linked":{"type":"boolean","description":"Whether this policy has been evaluated by decision logs"},"lastEvaluatedAt":{"type":"string","format":"date-time","nullable":true,"description":"ISO timestamp of most recent decision log evaluation"},"evaluationCount":{"type":"integer","minimum":0,"description":"Total number of decision log evaluations"}}}},"pagination":{"type":"object","required":["total","page","perPage","totalPages"],"properties":{"total":{"type":"integer","minimum":0},"page":{"type":"integer","minimum":1},"perPage":{"type":"integer","minimum":1},"totalPages":{"type":"integer","minimum":0}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies/{id}":{"get":{"summary":"Get a policy by ID","description":"Returns a single policy item by its generated ID.","operationId":"getPolicyById","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"entityId","in":"query","required":false,"description":"Entity ID for fallback lookup when policy hash has shifted","schema":{"type":"string","maxLength":255}},{"name":"fileName","in":"query","required":false,"description":"File name for fallback lookup when policy hash has shifted","schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Policy found","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","fileName","package","status","source","entityId","entityName","inheritedByCount","imposedOnCount","createdAt","lastEditedAt","lastInvokedAt","linked","lastEvaluatedAt","evaluationCount"],"properties":{"id":{"type":"string"},"fileName":{"type":"string"},"package":{"type":"string"},"status":{"type":"string","enum":["active","error","draft","disabled"]},"source":{"type":"string","enum":["indexed","non_indexed"]},"entityId":{"type":"string","format":"uuid"},"entityName":{"type":"string"},"inheritedByCount":{"type":"integer","minimum":0},"imposedOnCount":{"type":"integer","minimum":0},"createdAt":{"type":"string","format":"date-time"},"lastEditedAt":{"type":"string","format":"date-time"},"lastInvokedAt":{"type":"string","format":"date-time","nullable":true},"linked":{"type":"boolean"},"lastEvaluatedAt":{"type":"string","format":"date-time","nullable":true},"evaluationCount":{"type":"integer","minimum":0}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies/drafts/{id}":{"get":{"summary":"Load a policy draft for editing","description":"Fetches code from Git branch, PR state from GitHub API, and metadata from DB.\nRequires policies:read authorization.\n","operationId":"getPolicyDraft","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Draft loaded successfully","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","policyName","entityId","entityName","filePath","branch","code","status","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"policyName":{"type":"string"},"entityId":{"type":"string","format":"uuid"},"entityName":{"type":"string"},"filePath":{"type":"string"},"branch":{"type":"string"},"prNumber":{"type":"integer","nullable":true},"prUrl":{"type":"string","nullable":true},"commitSha":{"type":"string","nullable":true},"code":{"type":"string"},"status":{"type":"string"},"prStatus":{"type":"object","nullable":true,"properties":{"state":{"type":"string"},"checksStatus":{"type":"string","nullable":true},"reviewStatus":{"type":"string","nullable":true}}},"createdBy":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"summary":"Push an update to an existing policy draft","description":"Updates the policy code on the existing branch.\nValidates that the PR is still open before pushing.\nRequires policies:create authorization.\nSupports idempotency via the Idempotency-Key header.\n","operationId":"updatePolicyDraft","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Idempotency key for safe retries"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","minLength":1,"description":"Updated Rego policy code"},"description":{"type":"string","maxLength":2000,"description":"Optional commit message description"}}}}}},"responses":{"200":{"description":"Draft updated successfully","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["commitSha"],"properties":{"commitSha":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/policies/drafts/{id}/status":{"get":{"summary":"Poll policy draft status","description":"Lightweight status poll — no code fetch, just PR state from GitHub.\nAlso syncs status back to DB if merged/closed.\nRequires policies:read authorization.\n","operationId":"getPolicyDraftStatus","tags":["Policies"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Draft status","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["status","updatedAt"],"properties":{"status":{"type":"string"},"checksStatus":{"type":"string","nullable":true},"reviewStatus":{"type":"string","nullable":true},"prUrl":{"type":"string","nullable":true},"updatedAt":{"type":"string","format":"date-time"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/user-groups":{"get":{"summary":"List user groups","description":"Returns all active user groups for the authenticated tenant. Soft-deleted groups are\nexcluded. Results are ordered by creation time.\n","operationId":"listUserGroups","tags":["User Groups"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"User groups","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Create user group","description":"Creates a new user group within the tenant. Names are case-insensitive and unique within\nthe tenant.\n","operationId":"createUserGroup","tags":["User Groups"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Optional idempotency key for safe retry semantics."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupCreate"}}}},"responses":{"201":{"description":"User group created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"A group with this name already exists (case-insensitive).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/user-groups/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group identifier"}],"get":{"summary":"Get user group","operationId":"getUserGroup","tags":["User Groups"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"User group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"summary":"Update user group","description":"Rename a group and/or update its description.","operationId":"updateUserGroup","tags":["User Groups"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupUpdate"}}}},"responses":{"200":{"description":"Updated user group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A group with this name already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete user group","description":"Soft-deletes the group. Rejected with `group_has_members` if the group has any active\nmembers.\n","operationId":"deleteUserGroup","tags":["User Groups"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"204":{"description":"User group deleted"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Cannot delete a group with members.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupHasMembersError"}}}}}}},"/v1/user-groups/{id}/members":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User group identifier"}],"get":{"summary":"List group members","description":"Returns members of the group, paginated by `assignedAt DESC, id DESC`.\nThe cursor is the membership row id; it is opaque to clients.\n","operationId":"listGroupMembers","tags":["Group Memberships"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":50}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Group members","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"summary":"Add members to a group","description":"Adds 1..500 users to the group. Idempotent: re-issuing the same request\nmoves all `added` IDs into `alreadyMembers` on the second call. Each\nuserId is validated against the tenant's entity graph; any user not in\nthis tenant returns 400 `user_not_in_tenant` with `meta.invalidUserIds`.\nOne outbox event per call, regardless of bulk size.\n","operationId":"addGroupMembers","tags":["Group Memberships"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipUserIds"}}}},"responses":{"201":{"description":"Members added. The server always returns 201 for this operation,\nincluding idempotent replays where every requested userId is\nalready a member. Clients distinguish \"new additions\" from\n\"no-op replay\" by inspecting `added` (newly inserted) vs\n`alreadyMembers` (pre-existing) in the response body.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipAddResponse"}}}},"400":{"description":"Validation failed, the userIds list exceeded 500, or one or more\nuserIds are not in this tenant. The user_not_in_tenant variant\ncarries `meta.invalidUserIds`.\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/GroupMembershipUserNotInTenantError"},{"$ref":"#/components/schemas/Error"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/user-groups/{id}/members/{userId}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"delete":{"summary":"Remove a single user from a group","description":"Strict: returns 400 `not_a_member` with `meta.notMembers` if the userId\nis not currently a member. No partial behavior.\n","operationId":"removeGroupMember","tags":["Group Memberships"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"204":{"description":"Member removed"},"400":{"description":"User is not a member of this group.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipNotAMemberError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/user-groups/{id}/members/bulk-remove":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"summary":"Remove 1..500 users from a group (strict)","description":"All-or-nothing: if any requested userId is not a current member, returns\n400 `not_a_member` with `meta.notMembers` and no rows are removed.\n","operationId":"bulkRemoveGroupMembers","tags":["Group Memberships"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipUserIds"}}}},"responses":{"200":{"description":"Members removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipBulkRemoveResponse"}}}},"400":{"description":"Strict rejection — one or more userIds were not members.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupMembershipNotAMemberError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/group-memberships":{"get":{"summary":"Batch lookup of groups per user","description":"Returns a map of userId → array of groups the user belongs to. Used by\nthe User Management page Groups column to avoid per-row lookups. Users\nwith zero memberships return an empty array (a valid state).\n","operationId":"listUserGroupMemberships","tags":["Group Memberships"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"userIds","in":"query","required":true,"schema":{"type":"string","minLength":36},"description":"Comma-separated list of user UUIDs (1..500). Whitespace is trimmed\nand duplicates are deduped server-side.\n"}],"responses":{"200":{"description":"User → groups map","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserGroupMembershipsLookupResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/users":{"get":{"summary":"List tenant users (EA-896)","description":"Returns every user in the caller's tenant with derived status and the\nflat list of group + entity memberships needed by the User Management\npage (EA-869). Administrator role required — engineers receive 403.\n","operationId":"listTenantUsers","tags":["Users"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Tenant-wide user list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantUserListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/invitations":{"get":{"summary":"List tenant pending invitations (EA-896)","description":"Returns every pending invitation in the caller's tenant including the\ntarget entity name and role. Administrator role required — engineers\nreceive 403.\n","operationId":"listTenantInvitations","tags":["Invitations"],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Tenant-wide pending invitation list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantInvitationListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}