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>
640 lines
22 KiB
YAML
640 lines
22 KiB
YAML
openapi: "3.0.3"
|
|
|
|
info:
|
|
title: SentryAgent.ai — Identity Federation
|
|
version: 1.0.0
|
|
description: |
|
|
Cross-IdP identity federation endpoints for the SentryAgent.ai AgentIdP platform.
|
|
|
|
The federation subsystem enables this IdP to trust and verify JWT tokens issued by
|
|
external identity providers (partner IdPs). This allows agents from federated
|
|
organizations to authenticate without re-registering.
|
|
|
|
**Partner management** (`admin:orgs` scope required):
|
|
- `POST /federation/trust` — Register a new trusted partner IdP
|
|
- `GET /federation/partners` — List all registered partners
|
|
- `GET /federation/partners/:id` — Get a specific partner
|
|
- `PATCH /federation/partners/:id` — Update a partner
|
|
- `DELETE /federation/partners/:id` — Remove a partner
|
|
|
|
**Token verification** (any authenticated agent):
|
|
- `POST /federation/verify` — Verify a federated JWT from a trusted partner
|
|
|
|
servers:
|
|
- url: http://localhost:3000/api/v1
|
|
description: Local development server
|
|
- url: https://api.sentryagent.ai/v1
|
|
description: Production server
|
|
|
|
tags:
|
|
- name: Federation Partners
|
|
description: Trusted partner IdP management (admin:orgs scope)
|
|
- name: Federation Verification
|
|
description: Cross-IdP token verification (any authenticated agent)
|
|
|
|
components:
|
|
securitySchemes:
|
|
BearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
description: |
|
|
JWT access token obtained via `POST /token`.
|
|
Include as `Authorization: Bearer <token>`.
|
|
|
|
schemas:
|
|
FederationPartnerStatus:
|
|
type: string
|
|
enum:
|
|
- active
|
|
- suspended
|
|
- expired
|
|
description: |
|
|
Lifecycle status of a federation partner.
|
|
- `active` — partner is trusted; tokens accepted.
|
|
- `suspended` — temporarily disabled; tokens rejected.
|
|
- `expired` — `expires_at` has passed; tokens rejected.
|
|
example: active
|
|
|
|
FederationPartner:
|
|
type: object
|
|
description: A registered trusted federation partner IdP.
|
|
required:
|
|
- id
|
|
- name
|
|
- issuer
|
|
- jwks_uri
|
|
- allowed_organizations
|
|
- status
|
|
- created_at
|
|
- updated_at
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
description: Immutable system-assigned UUID for this partner.
|
|
readOnly: true
|
|
example: "fed-abcd-1234-5678-ef01"
|
|
name:
|
|
type: string
|
|
description: Human-readable partner name.
|
|
example: "Acme Partner IdP"
|
|
issuer:
|
|
type: string
|
|
format: uri
|
|
description: Issuer URL — must match the `iss` claim in federated tokens.
|
|
example: "https://idp.acme-partner.example"
|
|
jwks_uri:
|
|
type: string
|
|
format: uri
|
|
description: URL of the partner's JWKS endpoint for token signature verification.
|
|
example: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: |
|
|
Allowlist of organization_id values accepted from this partner.
|
|
An empty array means all organizations from this partner are accepted.
|
|
example: ["org-acme-001", "org-acme-002"]
|
|
status:
|
|
$ref: '#/components/schemas/FederationPartnerStatus'
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
readOnly: true
|
|
example: "2026-03-01T08:00:00.000Z"
|
|
updated_at:
|
|
type: string
|
|
format: date-time
|
|
readOnly: true
|
|
example: "2026-03-28T11:30:00.000Z"
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: Optional expiry timestamp. Null means the partner never expires.
|
|
example: "2027-01-01T00:00:00.000Z"
|
|
|
|
CreatePartnerRequest:
|
|
type: object
|
|
description: Request body for registering a new trusted federation partner.
|
|
required:
|
|
- name
|
|
- issuer
|
|
- jwks_uri
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: Human-readable partner name.
|
|
minLength: 1
|
|
maxLength: 256
|
|
example: "Acme Partner IdP"
|
|
issuer:
|
|
type: string
|
|
format: uri
|
|
description: Issuer URL of the external IdP. Must match `iss` in federated tokens.
|
|
example: "https://idp.acme-partner.example"
|
|
jwks_uri:
|
|
type: string
|
|
format: uri
|
|
description: |
|
|
URL of the partner's JWKS endpoint. Must be reachable at registration time —
|
|
the endpoint is probed before the partner is persisted.
|
|
example: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Optional allowlist of organization_id values. Defaults to empty (all allowed).
|
|
example: ["org-acme-001"]
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
description: Optional ISO 8601 date-time after which the partner will be automatically expired.
|
|
example: "2027-01-01T00:00:00.000Z"
|
|
|
|
UpdatePartnerRequest:
|
|
type: object
|
|
description: |
|
|
Request body for partially updating a federation partner.
|
|
All fields are optional; only provided fields are updated.
|
|
minProperties: 1
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 256
|
|
example: "Acme Partner IdP (Updated)"
|
|
jwks_uri:
|
|
type: string
|
|
format: uri
|
|
description: Updated JWKS endpoint URL. The JWKS cache will be invalidated on update.
|
|
example: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example: ["org-acme-001", "org-acme-003"]
|
|
status:
|
|
$ref: '#/components/schemas/FederationPartnerStatus'
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
description: Updated expiry. Pass null to remove the expiry.
|
|
example: "2028-01-01T00:00:00.000Z"
|
|
|
|
FederationVerifyRequest:
|
|
type: object
|
|
description: Request body for verifying a federated token from a trusted partner.
|
|
required:
|
|
- token
|
|
properties:
|
|
token:
|
|
type: string
|
|
description: The JWT token string issued by the partner IdP.
|
|
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0wMDEiLCJpc3MiOiJodHRwczovL2lkcC5hY21lLXBhcnRuZXIuZXhhbXBsZSIsImV4cCI6MTc0MzE1NDgwMH0.signature"
|
|
expected_issuer:
|
|
type: string
|
|
format: uri
|
|
description: |
|
|
Optional issuer hint. When provided, the token's `iss` claim must match exactly.
|
|
Useful to prevent issuer confusion when multiple partners share JWKS.
|
|
example: "https://idp.acme-partner.example"
|
|
|
|
FederationVerifyResult:
|
|
type: object
|
|
description: Result of a successful federated token verification.
|
|
required:
|
|
- valid
|
|
- issuer
|
|
- subject
|
|
- claims
|
|
properties:
|
|
valid:
|
|
type: boolean
|
|
description: Whether the token is valid (true when verification passed).
|
|
example: true
|
|
issuer:
|
|
type: string
|
|
format: uri
|
|
description: The `iss` claim from the verified token.
|
|
example: "https://idp.acme-partner.example"
|
|
subject:
|
|
type: string
|
|
description: The `sub` claim from the verified token.
|
|
example: "agent-001-external"
|
|
organization_id:
|
|
type: string
|
|
description: The `organization_id` claim from the token, if present.
|
|
example: "org-acme-001"
|
|
claims:
|
|
type: object
|
|
description: All claims from the verified token payload.
|
|
additionalProperties: true
|
|
example:
|
|
iss: "https://idp.acme-partner.example"
|
|
sub: "agent-001-external"
|
|
aud: "https://api.sentryagent.ai"
|
|
iat: 1743151200
|
|
exp: 1743154800
|
|
organization_id: "org-acme-001"
|
|
|
|
ErrorResponse:
|
|
type: object
|
|
description: Standard error response envelope.
|
|
required:
|
|
- code
|
|
- message
|
|
properties:
|
|
code:
|
|
type: string
|
|
example: "FEDERATION_PARTNER_NOT_FOUND"
|
|
message:
|
|
type: string
|
|
example: "Federation partner with the specified ID was not found."
|
|
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: Federation partner not found.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "FEDERATION_PARTNER_NOT_FOUND"
|
|
message: "Federation partner with the specified ID was not found."
|
|
|
|
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:
|
|
/federation/trust:
|
|
post:
|
|
operationId: registerFederationPartner
|
|
tags:
|
|
- Federation Partners
|
|
summary: Register a trusted federation partner
|
|
description: |
|
|
Registers a new external IdP as a trusted federation partner.
|
|
|
|
The partner's JWKS endpoint (`jwks_uri`) is probed at registration time
|
|
to confirm it is reachable. If the endpoint is unreachable, the request
|
|
is rejected with `422 Unprocessable Entity`.
|
|
|
|
Requires `admin:orgs` scope.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreatePartnerRequest'
|
|
example:
|
|
name: "Acme Partner IdP"
|
|
issuer: "https://idp.acme-partner.example"
|
|
jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations:
|
|
- "org-acme-001"
|
|
expires_at: "2027-01-01T00:00:00.000Z"
|
|
responses:
|
|
'201':
|
|
description: Federation partner registered successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FederationPartner'
|
|
example:
|
|
id: "fed-abcd-1234-5678-ef01"
|
|
name: "Acme Partner IdP"
|
|
issuer: "https://idp.acme-partner.example"
|
|
jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations:
|
|
- "org-acme-001"
|
|
status: "active"
|
|
created_at: "2026-04-07T09:00:00.000Z"
|
|
updated_at: "2026-04-07T09:00:00.000Z"
|
|
expires_at: "2027-01-01T00: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: "jwks_uri"
|
|
reason: "Must be a valid HTTPS URL."
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'409':
|
|
description: A partner with the same issuer is already registered.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "FEDERATION_PARTNER_CONFLICT"
|
|
message: "A federation partner with this issuer is already registered."
|
|
'422':
|
|
description: Partner JWKS endpoint is unreachable.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "JWKS_UNREACHABLE"
|
|
message: "The partner JWKS endpoint is unreachable. Verify the URL and try again."
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
/federation/partners:
|
|
get:
|
|
operationId: listFederationPartners
|
|
tags:
|
|
- Federation Partners
|
|
summary: List all federation partners
|
|
description: |
|
|
Returns all registered federation partners.
|
|
Partners past their `expires_at` are returned with status `expired`.
|
|
|
|
Requires `admin:orgs` scope.
|
|
responses:
|
|
'200':
|
|
description: List of federation partners returned successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FederationPartner'
|
|
example:
|
|
- id: "fed-abcd-1234-5678-ef01"
|
|
name: "Acme Partner IdP"
|
|
issuer: "https://idp.acme-partner.example"
|
|
jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations: ["org-acme-001"]
|
|
status: "active"
|
|
created_at: "2026-03-01T08:00:00.000Z"
|
|
updated_at: "2026-03-28T11:30:00.000Z"
|
|
expires_at: "2027-01-01T00:00:00.000Z"
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
/federation/partners/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
description: UUID of the federation partner.
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: "fed-abcd-1234-5678-ef01"
|
|
|
|
get:
|
|
operationId: getFederationPartner
|
|
tags:
|
|
- Federation Partners
|
|
summary: Get a federation partner by ID
|
|
description: |
|
|
Returns a single federation partner record by its UUID.
|
|
Requires `admin:orgs` scope.
|
|
responses:
|
|
'200':
|
|
description: Federation partner returned successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FederationPartner'
|
|
example:
|
|
id: "fed-abcd-1234-5678-ef01"
|
|
name: "Acme Partner IdP"
|
|
issuer: "https://idp.acme-partner.example"
|
|
jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations: ["org-acme-001"]
|
|
status: "active"
|
|
created_at: "2026-03-01T08:00:00.000Z"
|
|
updated_at: "2026-03-28T11:30:00.000Z"
|
|
expires_at: null
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
patch:
|
|
operationId: updateFederationPartner
|
|
tags:
|
|
- Federation Partners
|
|
summary: Update a federation partner
|
|
description: |
|
|
Partially updates a federation partner's configuration.
|
|
Only provided fields are updated.
|
|
|
|
Changing `jwks_uri` invalidates the cached JWKS for this partner.
|
|
Requires `admin:orgs` scope.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdatePartnerRequest'
|
|
example:
|
|
status: "suspended"
|
|
allowed_organizations:
|
|
- "org-acme-001"
|
|
- "org-acme-003"
|
|
responses:
|
|
'200':
|
|
description: Federation partner updated successfully.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FederationPartner'
|
|
example:
|
|
id: "fed-abcd-1234-5678-ef01"
|
|
name: "Acme Partner IdP"
|
|
issuer: "https://idp.acme-partner.example"
|
|
jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json"
|
|
allowed_organizations: ["org-acme-001", "org-acme-003"]
|
|
status: "suspended"
|
|
created_at: "2026-03-01T08:00:00.000Z"
|
|
updated_at: "2026-04-07T09:00:00.000Z"
|
|
expires_at: null
|
|
'400':
|
|
description: Validation error in request body.
|
|
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'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
delete:
|
|
operationId: deleteFederationPartner
|
|
tags:
|
|
- Federation Partners
|
|
summary: Remove a federation partner
|
|
description: |
|
|
Permanently removes a federation partner. After removal, tokens issued
|
|
by the partner's IdP will no longer be accepted.
|
|
|
|
Requires `admin:orgs` scope.
|
|
responses:
|
|
'204':
|
|
description: Federation partner removed successfully. No response body.
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|
|
|
|
/federation/verify:
|
|
post:
|
|
operationId: verifyFederatedToken
|
|
tags:
|
|
- Federation Verification
|
|
summary: Verify a federated JWT token
|
|
description: |
|
|
Verifies a JWT token issued by a registered federation partner.
|
|
|
|
The token's `iss` claim is matched against registered active partners.
|
|
The signature is verified against the partner's JWKS.
|
|
The partner's `allowed_organizations` filter is applied if non-empty.
|
|
|
|
Returns verification result with claims on success.
|
|
Returns `422` with verification failure details on invalid tokens.
|
|
|
|
Requires any valid Bearer token (`agents:read` scope is sufficient).
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FederationVerifyRequest'
|
|
example:
|
|
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0wMDEiLCJpc3MiOiJodHRwczovL2lkcC5hY21lLXBhcnRuZXIuZXhhbXBsZSJ9.signature"
|
|
expected_issuer: "https://idp.acme-partner.example"
|
|
responses:
|
|
'200':
|
|
description: Token verification result returned.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FederationVerifyResult'
|
|
examples:
|
|
valid:
|
|
summary: Token verified successfully
|
|
value:
|
|
valid: true
|
|
issuer: "https://idp.acme-partner.example"
|
|
subject: "agent-001-external"
|
|
organization_id: "org-acme-001"
|
|
claims:
|
|
iss: "https://idp.acme-partner.example"
|
|
sub: "agent-001-external"
|
|
aud: "https://api.sentryagent.ai"
|
|
iat: 1743151200
|
|
exp: 1743154800
|
|
invalid:
|
|
summary: Token invalid or expired
|
|
value:
|
|
valid: false
|
|
issuer: ""
|
|
subject: ""
|
|
claims: {}
|
|
'400':
|
|
description: Missing or malformed token in request body.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "VALIDATION_ERROR"
|
|
message: "The 'token' field is required."
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'422':
|
|
description: |
|
|
Token is well-formed but verification failed (unknown issuer, expired,
|
|
invalid signature, or organization not in allowlist).
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
examples:
|
|
unknownIssuer:
|
|
summary: Unknown issuer
|
|
value:
|
|
code: "UNKNOWN_FEDERATION_ISSUER"
|
|
message: "No active federation partner found for this token issuer."
|
|
partnerSuspended:
|
|
summary: Partner is suspended
|
|
value:
|
|
code: "FEDERATION_PARTNER_SUSPENDED"
|
|
message: "The federation partner for this token issuer is suspended."
|
|
orgNotAllowed:
|
|
summary: Organization not in allowlist
|
|
value:
|
|
code: "FEDERATION_ORG_NOT_ALLOWED"
|
|
message: "The token's organization_id is not in the partner's allowed_organizations list."
|
|
'500':
|
|
$ref: '#/components/responses/InternalServerError'
|