All findings from the inaugural LeadValidator audit resolved and confirmed. Release gate: PASS. VV_ISSUE_002 (BLOCKER): 15 OpenAPI specs verified present covering all 20 route groups (46 endpoints documented in docs/openapi/) VV_ISSUE_003 (MAJOR): Remove any types from src/db/pool.ts — replaced pool.query shim with unknown[] + Object.defineProperty, zero any types, eslint-disable suppressions removed VV_ISSUE_004 (MAJOR): Remove raw Pool from ScaffoldController and HealthDetailedController — injected AgentRepository/CredentialRepository and DbProbe interface respectively; added CredentialRepository.findActiveClientId() VV_ISSUE_005 (MAJOR): Add unit tests for 5 untested services — ComplianceStatusStore, EventPublisher, MarketplaceService, OIDCTrustPolicyService, UsageService VV_ISSUE_006 (MAJOR): Add integration tests for 7 missing route groups — analytics, billing, tiers, webhooks, marketplace, oidc-trust-policies, oidc-token-exchange VV_ISSUE_001 (MINOR): Create missing design.md and tasks.md in 4 OpenSpec archives — all archives now complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
708 lines
21 KiB
YAML
708 lines
21 KiB
YAML
openapi: "3.0.3"
|
|
|
|
info:
|
|
title: SentryAgent.ai — Organizations (Multi-Tenancy)
|
|
version: 1.0.0
|
|
description: |
|
|
Organization (tenant) management endpoints for the SentryAgent.ai AgentIdP platform.
|
|
|
|
Organizations are the top-level multi-tenancy boundary. Each organization has its
|
|
own pool of registered agents, plan-tier limits, and billing context.
|
|
|
|
**All endpoints require a valid Bearer JWT** with appropriate OPA-enforced scope.
|
|
|
|
**Plan Tiers:**
|
|
| Tier | Max Agents | Max Tokens/Month |
|
|
|------|-----------|-----------------|
|
|
| `free` | 100 | 10,000 |
|
|
| `pro` | 1,000 | 100,000 |
|
|
| `enterprise` | unlimited | unlimited |
|
|
|
|
servers:
|
|
- url: http://localhost:3000/api/v1
|
|
description: Local development server
|
|
- url: https://api.sentryagent.ai/v1
|
|
description: Production server
|
|
|
|
tags:
|
|
- name: Organizations
|
|
description: CRUD operations for organizations (tenants)
|
|
- name: Organization Members
|
|
description: Agent membership management within organizations
|
|
|
|
components:
|
|
securitySchemes:
|
|
BearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
description: |
|
|
JWT access token obtained via `POST /token`.
|
|
Include as `Authorization: Bearer <token>`.
|
|
|
|
schemas:
|
|
PlanTier:
|
|
type: string
|
|
enum:
|
|
- free
|
|
- pro
|
|
- enterprise
|
|
description: Subscription plan tier for an organization.
|
|
example: pro
|
|
|
|
OrgStatus:
|
|
type: string
|
|
enum:
|
|
- active
|
|
- suspended
|
|
- deleted
|
|
description: Lifecycle status of an organization.
|
|
example: active
|
|
|
|
OrgRole:
|
|
type: string
|
|
enum:
|
|
- member
|
|
- admin
|
|
description: Role of an agent within an organization.
|
|
example: member
|
|
|
|
Organization:
|
|
type: object
|
|
description: Full representation of a registered organization (tenant).
|
|
required:
|
|
- organizationId
|
|
- name
|
|
- slug
|
|
- planTier
|
|
- maxAgents
|
|
- maxTokensPerMonth
|
|
- status
|
|
- createdAt
|
|
- updatedAt
|
|
properties:
|
|
organizationId:
|
|
type: string
|
|
format: uuid
|
|
description: Immutable, system-assigned unique identifier for the organization.
|
|
readOnly: true
|
|
example: "org-1234-5678-abcd-ef01"
|
|
name:
|
|
type: string
|
|
description: Human-readable display name of the organization.
|
|
minLength: 1
|
|
maxLength: 256
|
|
example: "Acme Corp"
|
|
slug:
|
|
type: string
|
|
description: |
|
|
URL-friendly unique identifier. Lowercase letters, digits, and hyphens only.
|
|
Immutable after creation.
|
|
pattern: '^[a-z0-9-]+$'
|
|
example: "acme-corp"
|
|
planTier:
|
|
$ref: '#/components/schemas/PlanTier'
|
|
maxAgents:
|
|
type: integer
|
|
description: Maximum number of agents permitted in this organization.
|
|
minimum: 1
|
|
example: 100
|
|
maxTokensPerMonth:
|
|
type: integer
|
|
description: Maximum OAuth 2.0 token requests per calendar month.
|
|
minimum: 1
|
|
example: 10000
|
|
status:
|
|
$ref: '#/components/schemas/OrgStatus'
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
readOnly: true
|
|
example: "2026-03-01T08:00:00.000Z"
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
readOnly: true
|
|
example: "2026-03-28T11:30:00.000Z"
|
|
|
|
CreateOrgRequest:
|
|
type: object
|
|
description: Request body for creating a new organization.
|
|
required:
|
|
- name
|
|
- slug
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: Human-readable display name.
|
|
minLength: 1
|
|
maxLength: 256
|
|
example: "Acme Corp"
|
|
slug:
|
|
type: string
|
|
description: URL-friendly unique identifier. Lowercase letters, digits, hyphens only.
|
|
pattern: '^[a-z0-9-]+$'
|
|
minLength: 1
|
|
maxLength: 64
|
|
example: "acme-corp"
|
|
planTier:
|
|
$ref: '#/components/schemas/PlanTier'
|
|
description: Defaults to `free` when omitted.
|
|
maxAgents:
|
|
type: integer
|
|
description: Override the default max agents for this plan tier.
|
|
minimum: 1
|
|
example: 100
|
|
maxTokensPerMonth:
|
|
type: integer
|
|
description: Override the default max tokens per month.
|
|
minimum: 1
|
|
example: 10000
|
|
|
|
UpdateOrgRequest:
|
|
type: object
|
|
description: |
|
|
Request body for partially updating an organization.
|
|
All fields are optional; only provided fields are updated.
|
|
`status` may only be set to `active` or `suspended` via this endpoint;
|
|
`deleted` is applied only through the DELETE operation.
|
|
minProperties: 1
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 256
|
|
example: "Acme Corporation"
|
|
planTier:
|
|
$ref: '#/components/schemas/PlanTier'
|
|
maxAgents:
|
|
type: integer
|
|
minimum: 1
|
|
example: 1000
|
|
maxTokensPerMonth:
|
|
type: integer
|
|
minimum: 1
|
|
example: 100000
|
|
status:
|
|
type: string
|
|
enum:
|
|
- active
|
|
- suspended
|
|
description: Set to `active` to reactivate a suspended org, or `suspended` to disable it.
|
|
example: active
|
|
|
|
PaginatedOrgsResponse:
|
|
type: object
|
|
description: Paginated list of organizations.
|
|
required:
|
|
- data
|
|
- total
|
|
- page
|
|
- limit
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Organization'
|
|
total:
|
|
type: integer
|
|
example: 42
|
|
page:
|
|
type: integer
|
|
example: 1
|
|
limit:
|
|
type: integer
|
|
example: 20
|
|
|
|
OrgMember:
|
|
type: object
|
|
description: An agent's membership record within an organization.
|
|
required:
|
|
- memberId
|
|
- organizationId
|
|
- agentId
|
|
- role
|
|
- joinedAt
|
|
properties:
|
|
memberId:
|
|
type: string
|
|
format: uuid
|
|
readOnly: true
|
|
example: "mem-abcd-1234-5678-ef01"
|
|
organizationId:
|
|
type: string
|
|
format: uuid
|
|
example: "org-1234-5678-abcd-ef01"
|
|
agentId:
|
|
type: string
|
|
format: uuid
|
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
role:
|
|
$ref: '#/components/schemas/OrgRole'
|
|
joinedAt:
|
|
type: string
|
|
format: date-time
|
|
readOnly: true
|
|
example: "2026-03-28T09:00:00.000Z"
|
|
|
|
AddMemberRequest:
|
|
type: object
|
|
description: Request body for adding an agent to an organization.
|
|
required:
|
|
- agentId
|
|
- role
|
|
properties:
|
|
agentId:
|
|
type: string
|
|
format: uuid
|
|
description: UUID of the agent to add.
|
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
role:
|
|
$ref: '#/components/schemas/OrgRole'
|
|
|
|
ErrorResponse:
|
|
type: object
|
|
description: Standard error response envelope.
|
|
required:
|
|
- code
|
|
- message
|
|
properties:
|
|
code:
|
|
type: string
|
|
example: "VALIDATION_ERROR"
|
|
message:
|
|
type: string
|
|
example: "Request validation failed."
|
|
details:
|
|
type: object
|
|
additionalProperties: true
|
|
|
|
responses:
|
|
Unauthorized:
|
|
description: Missing or invalid Bearer token.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "UNAUTHORIZED"
|
|
message: "A valid Bearer token is required to access this resource."
|
|
|
|
Forbidden:
|
|
description: Valid token but insufficient permissions.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "FORBIDDEN"
|
|
message: "You do not have permission to perform this action."
|
|
|
|
NotFound:
|
|
description: Organization not found.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "ORG_NOT_FOUND"
|
|
message: "Organization with the specified ID was not found."
|
|
|
|
TooManyRequests:
|
|
description: Rate limit exceeded.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "RATE_LIMIT_EXCEEDED"
|
|
message: "Too many requests. Please retry after the rate limit window resets."
|
|
|
|
InternalServerError:
|
|
description: Unexpected server error.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "INTERNAL_SERVER_ERROR"
|
|
message: "An unexpected error occurred. Please try again later."
|
|
|
|
security:
|
|
- BearerAuth: []
|
|
|
|
paths:
|
|
/organizations:
|
|
post:
|
|
operationId: createOrganization
|
|
tags:
|
|
- Organizations
|
|
summary: Create a new organization
|
|
description: |
|
|
Creates a new organization (tenant). The `slug` must be unique across all organizations
|
|
and is immutable after creation.
|
|
|
|
The requesting agent's organization context is determined from the Bearer token.
|
|
Requires `admin:orgs` scope.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateOrgRequest'
|
|
example:
|
|
name: "Acme Corp"
|
|
slug: "acme-corp"
|
|
planTier: "pro"
|
|
maxAgents: 500
|
|
maxTokensPerMonth: 50000
|
|
responses:
|
|
'201':
|
|
description: Organization created successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Organization'
|
|
example:
|
|
organizationId: "org-1234-5678-abcd-ef01"
|
|
name: "Acme Corp"
|
|
slug: "acme-corp"
|
|
planTier: "pro"
|
|
maxAgents: 500
|
|
maxTokensPerMonth: 50000
|
|
status: "active"
|
|
createdAt: "2026-04-07T09:00:00.000Z"
|
|
updatedAt: "2026-04-07T09:00:00.000Z"
|
|
'400':
|
|
description: Validation error in request body.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "VALIDATION_ERROR"
|
|
message: "Request validation failed."
|
|
details:
|
|
field: "slug"
|
|
reason: "slug must contain only lowercase letters, digits, and hyphens."
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'409':
|
|
description: An organization with the provided slug already exists.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "ORG_SLUG_CONFLICT"
|
|
message: "An organization with this slug already exists."
|
|
details:
|
|
slug: "acme-corp"
|
|
'429':
|
|
$ref: '#/components/responses/TooManyRequests'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
get:
|
|
operationId: listOrganizations
|
|
tags:
|
|
- Organizations
|
|
summary: List organizations
|
|
description: |
|
|
Returns a paginated list of organizations. Results can be filtered by `status`.
|
|
Results are ordered by `createdAt` descending.
|
|
|
|
Requires `admin:orgs` scope.
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
default: 1
|
|
example: 1
|
|
- name: limit
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 20
|
|
example: 20
|
|
- name: status
|
|
in: query
|
|
required: false
|
|
schema:
|
|
$ref: '#/components/schemas/OrgStatus'
|
|
responses:
|
|
'200':
|
|
description: Paginated list of organizations returned successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PaginatedOrgsResponse'
|
|
example:
|
|
data:
|
|
- organizationId: "org-1234-5678-abcd-ef01"
|
|
name: "Acme Corp"
|
|
slug: "acme-corp"
|
|
planTier: "pro"
|
|
maxAgents: 500
|
|
maxTokensPerMonth: 50000
|
|
status: "active"
|
|
createdAt: "2026-03-01T08:00:00.000Z"
|
|
updatedAt: "2026-03-28T11:30:00.000Z"
|
|
total: 42
|
|
page: 1
|
|
limit: 20
|
|
'400':
|
|
description: Invalid query parameters.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "VALIDATION_ERROR"
|
|
message: "Invalid query parameter value."
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'429':
|
|
$ref: '#/components/responses/TooManyRequests'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
/organizations/{orgId}:
|
|
parameters:
|
|
- name: orgId
|
|
in: path
|
|
required: true
|
|
description: The unique UUID identifier of the organization.
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: "org-1234-5678-abcd-ef01"
|
|
|
|
get:
|
|
operationId: getOrganization
|
|
tags:
|
|
- Organizations
|
|
summary: Get organization by ID
|
|
description: |
|
|
Retrieves the full record for a single organization.
|
|
Requires `admin:orgs` scope.
|
|
responses:
|
|
'200':
|
|
description: Organization record returned successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Organization'
|
|
example:
|
|
organizationId: "org-1234-5678-abcd-ef01"
|
|
name: "Acme Corp"
|
|
slug: "acme-corp"
|
|
planTier: "pro"
|
|
maxAgents: 500
|
|
maxTokensPerMonth: 50000
|
|
status: "active"
|
|
createdAt: "2026-03-01T08:00:00.000Z"
|
|
updatedAt: "2026-03-28T11:30:00.000Z"
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
patch:
|
|
operationId: updateOrganization
|
|
tags:
|
|
- Organizations
|
|
summary: Update organization metadata
|
|
description: |
|
|
Partially updates an organization's metadata.
|
|
Only provided fields are updated; omitted fields are unchanged.
|
|
`slug` and `organizationId` are immutable.
|
|
Requires `admin:orgs` scope.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateOrgRequest'
|
|
example:
|
|
name: "Acme Corporation"
|
|
planTier: "enterprise"
|
|
responses:
|
|
'200':
|
|
description: Organization updated successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Organization'
|
|
example:
|
|
organizationId: "org-1234-5678-abcd-ef01"
|
|
name: "Acme Corporation"
|
|
slug: "acme-corp"
|
|
planTier: "enterprise"
|
|
maxAgents: 500
|
|
maxTokensPerMonth: 50000
|
|
status: "active"
|
|
createdAt: "2026-03-01T08:00:00.000Z"
|
|
updatedAt: "2026-04-07T09:00:00.000Z"
|
|
'400':
|
|
description: Validation error or attempt to modify an immutable field.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "VALIDATION_ERROR"
|
|
message: "Request validation failed."
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/TooManyRequests'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
delete:
|
|
operationId: deleteOrganization
|
|
tags:
|
|
- Organizations
|
|
summary: Soft-delete an organization
|
|
description: |
|
|
Permanently soft-deletes an organization by setting its status to `deleted`.
|
|
The record is retained for audit purposes.
|
|
|
|
**Effects:**
|
|
- All agents within the organization are suspended.
|
|
- No new tokens may be issued for agents in this organization.
|
|
- This operation is **irreversible** via the API.
|
|
|
|
Requires `admin:orgs` scope.
|
|
responses:
|
|
'204':
|
|
description: Organization soft-deleted successfully. No response body.
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
description: Organization is already deleted.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "ORG_ALREADY_DELETED"
|
|
message: "This organization has already been deleted."
|
|
'429':
|
|
$ref: '#/components/responses/TooManyRequests'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
/organizations/{orgId}/members:
|
|
parameters:
|
|
- name: orgId
|
|
in: path
|
|
required: true
|
|
description: The unique UUID identifier of the organization.
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: "org-1234-5678-abcd-ef01"
|
|
|
|
post:
|
|
operationId: addOrganizationMember
|
|
tags:
|
|
- Organization Members
|
|
summary: Add an agent as a member of an organization
|
|
description: |
|
|
Adds a registered agent to an organization with the specified role.
|
|
The agent must already be registered in the system.
|
|
|
|
Requires `admin:orgs` scope.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AddMemberRequest'
|
|
example:
|
|
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
role: "member"
|
|
responses:
|
|
'201':
|
|
description: Agent added to organization successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/OrgMember'
|
|
example:
|
|
memberId: "mem-abcd-1234-5678-ef01"
|
|
organizationId: "org-1234-5678-abcd-ef01"
|
|
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
role: "member"
|
|
joinedAt: "2026-04-07T09:00:00.000Z"
|
|
'400':
|
|
description: Validation error in request body.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "VALIDATION_ERROR"
|
|
message: "Request validation failed."
|
|
details:
|
|
field: "role"
|
|
reason: "role must be one of: member, admin."
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
description: Organization or agent not found.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
examples:
|
|
orgNotFound:
|
|
summary: Organization not found
|
|
value:
|
|
code: "ORG_NOT_FOUND"
|
|
message: "Organization with the specified ID was not found."
|
|
agentNotFound:
|
|
summary: Agent not found
|
|
value:
|
|
code: "AGENT_NOT_FOUND"
|
|
message: "Agent with the specified ID was not found."
|
|
'409':
|
|
description: Agent is already a member of this organization.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "ALREADY_MEMBER"
|
|
message: "The agent is already a member of this organization."
|
|
'429':
|
|
$ref: '#/components/responses/TooManyRequests'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|