fix(vv): resolve all 6 V&V issues — field trial unblocked

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>
This commit is contained in:
SentryAgent.ai Developer
2026-04-07 04:52:47 +00:00
parent d216096dfb
commit 7441c9f298
49 changed files with 8954 additions and 70 deletions

676
docs/openapi/webhooks.yaml Normal file
View File

@@ -0,0 +1,676 @@
openapi: "3.0.3"
info:
title: SentryAgent.ai — Webhooks & Event Subscriptions
version: 1.0.0
description: |
Webhook subscription management and delivery history endpoints for the
SentryAgent.ai AgentIdP platform.
Webhooks deliver real-time event notifications to registered HTTP endpoints
when significant platform events occur (agent lifecycle changes, credential
operations, token events).
**All endpoints require a valid Bearer JWT** with appropriate scope:
- `webhooks:read` — required for GET operations
- `webhooks:write` — required for POST, PATCH, DELETE operations
**Delivery mechanism:**
- Events are delivered via `POST` to the registered `url`
- Each delivery carries a signed JSON envelope (`X-SentryAgent-Signature` header)
- Failed deliveries are retried with exponential backoff (up to 10 attempts)
- After 10 failures the subscription is marked `dead_letter`
**Supported event types:**
`agent.created`, `agent.updated`, `agent.suspended`, `agent.reactivated`,
`agent.decommissioned`, `credential.generated`, `credential.rotated`,
`credential.revoked`, `token.issued`, `token.revoked`
servers:
- url: http://localhost:3000/api/v1
description: Local development server
- url: https://api.sentryagent.ai/v1
description: Production server
tags:
- name: Webhook Subscriptions
description: Create and manage webhook endpoint subscriptions
- name: Webhook Deliveries
description: Query delivery history and attempt records
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT access token obtained via `POST /token`.
Include as `Authorization: Bearer <token>`.
schemas:
WebhookEventType:
type: string
enum:
- agent.created
- agent.updated
- agent.suspended
- agent.reactivated
- agent.decommissioned
- credential.generated
- credential.rotated
- credential.revoked
- token.issued
- token.revoked
description: Platform event type that can be subscribed to.
example: agent.created
WebhookDeliveryStatus:
type: string
enum:
- pending
- delivered
- failed
- dead_letter
description: |
Current status of a delivery attempt.
- `pending` — queued for delivery or awaiting retry
- `delivered` — successfully delivered (HTTP 2xx from target)
- `failed` — delivery failed; retry scheduled
- `dead_letter` — all retries exhausted; no further attempts
example: delivered
WebhookSubscription:
type: object
description: A registered webhook subscription (signing secret never included in responses).
required:
- id
- organization_id
- name
- url
- events
- active
- failure_count
- created_at
- updated_at
properties:
id:
type: string
format: uuid
description: Immutable system-assigned UUID for this subscription.
readOnly: true
example: "wh-abcd-1234-5678-ef01"
organization_id:
type: string
format: uuid
description: Organization that owns this subscription.
readOnly: true
example: "org-1234-5678-abcd-ef01"
name:
type: string
description: Human-readable label for this subscription.
example: "Agent lifecycle events"
url:
type: string
format: uri
description: HTTPS endpoint that receives webhook payloads.
example: "https://my-app.example.com/webhooks/sentryagent"
events:
type: array
items:
$ref: '#/components/schemas/WebhookEventType'
description: List of event types this subscription receives.
minItems: 1
example:
- agent.created
- agent.decommissioned
active:
type: boolean
description: Whether the subscription is currently active. Set to false to pause delivery.
example: true
failure_count:
type: integer
description: Number of consecutive delivery failures since last success.
minimum: 0
readOnly: true
example: 0
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"
CreateWebhookRequest:
type: object
description: Request body for creating a new webhook subscription.
required:
- name
- url
- events
properties:
name:
type: string
description: Human-readable label for this subscription.
minLength: 1
maxLength: 256
example: "Agent lifecycle events"
url:
type: string
format: uri
description: HTTPS endpoint URL. Must be HTTPS in production.
example: "https://my-app.example.com/webhooks/sentryagent"
events:
type: array
items:
$ref: '#/components/schemas/WebhookEventType'
minItems: 1
description: Event types to subscribe to.
example:
- agent.created
- agent.decommissioned
UpdateWebhookRequest:
type: object
description: |
Request body for partially updating a webhook subscription.
All fields are optional; only provided fields are updated.
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 256
example: "Agent events (updated)"
url:
type: string
format: uri
example: "https://my-app.example.com/webhooks/v2"
events:
type: array
items:
$ref: '#/components/schemas/WebhookEventType'
minItems: 1
example:
- agent.created
- agent.updated
- token.issued
active:
type: boolean
description: Set to false to pause delivery without deleting the subscription.
example: true
WebhookDelivery:
type: object
description: A single webhook delivery attempt record.
required:
- id
- subscription_id
- event_type
- payload
- status
- attempt_count
- created_at
- updated_at
properties:
id:
type: string
format: uuid
readOnly: true
example: "del-abcd-1234-5678-ef01"
subscription_id:
type: string
format: uuid
example: "wh-abcd-1234-5678-ef01"
event_type:
$ref: '#/components/schemas/WebhookEventType'
payload:
type: object
description: The JSON payload that was sent (or attempted) to the target URL.
additionalProperties: true
example:
id: "evt-1234-5678"
event: "agent.created"
timestamp: "2026-04-07T09:00:00.000Z"
organization_id: "org-1234-5678-abcd-ef01"
data:
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
status:
$ref: '#/components/schemas/WebhookDeliveryStatus'
http_status_code:
type: integer
nullable: true
description: HTTP status code returned by the target endpoint (null if connection failed).
example: 200
attempt_count:
type: integer
description: Number of delivery attempts made so far.
minimum: 1
example: 1
next_retry_at:
type: string
format: date-time
nullable: true
description: Scheduled time for the next retry attempt. Null if delivered or dead-lettered.
example: null
delivered_at:
type: string
format: date-time
nullable: true
description: Timestamp when the delivery was successfully confirmed.
example: "2026-04-07T09:00:05.000Z"
created_at:
type: string
format: date-time
readOnly: true
example: "2026-04-07T09:00:00.000Z"
updated_at:
type: string
format: date-time
readOnly: true
example: "2026-04-07T09:00:05.000Z"
PaginatedDeliveriesResponse:
type: object
description: Paginated delivery history response.
required:
- deliveries
- total
- limit
- offset
properties:
deliveries:
type: array
items:
$ref: '#/components/schemas/WebhookDelivery'
total:
type: integer
example: 87
limit:
type: integer
example: 20
offset:
type: integer
example: 0
ErrorResponse:
type: object
description: Standard error response envelope.
required:
- code
- message
properties:
code:
type: string
example: "WEBHOOK_NOT_FOUND"
message:
type: string
example: "Webhook subscription 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 scope.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: "FORBIDDEN"
message: "You do not have permission to perform this action."
NotFound:
description: Webhook subscription not found.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: "WEBHOOK_NOT_FOUND"
message: "Webhook subscription 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:
/webhooks:
post:
operationId: createWebhookSubscription
tags:
- Webhook Subscriptions
summary: Create a webhook subscription
description: |
Creates a new webhook subscription for the authenticated organization.
A signing secret is generated automatically and returned once in the response
under a `signingSecret` field. **Store this secret securely — it cannot be retrieved again.**
Requires `webhooks:write` scope.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateWebhookRequest'
example:
name: "Agent lifecycle events"
url: "https://my-app.example.com/webhooks/sentryagent"
events:
- agent.created
- agent.decommissioned
responses:
'201':
description: Webhook subscription created successfully.
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/WebhookSubscription'
- type: object
properties:
signingSecret:
type: string
description: |
HMAC signing secret for verifying webhook payloads.
Returned only once at creation time.
example: "whsec_abcdef1234567890abcdef1234567890"
example:
id: "wh-abcd-1234-5678-ef01"
organization_id: "org-1234-5678-abcd-ef01"
name: "Agent lifecycle events"
url: "https://my-app.example.com/webhooks/sentryagent"
events:
- agent.created
- agent.decommissioned
active: true
failure_count: 0
created_at: "2026-04-07T09:00:00.000Z"
updated_at: "2026-04-07T09:00:00.000Z"
signingSecret: "whsec_abcdef1234567890abcdef1234567890"
'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: "url"
reason: "Must be a valid HTTPS URL."
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
get:
operationId: listWebhookSubscriptions
tags:
- Webhook Subscriptions
summary: List webhook subscriptions
description: |
Returns all webhook subscriptions for the authenticated organization.
Requires `webhooks:read` scope.
responses:
'200':
description: List of webhook subscriptions returned successfully.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/WebhookSubscription'
example:
- id: "wh-abcd-1234-5678-ef01"
organization_id: "org-1234-5678-abcd-ef01"
name: "Agent lifecycle events"
url: "https://my-app.example.com/webhooks/sentryagent"
events:
- agent.created
- agent.decommissioned
active: true
failure_count: 0
created_at: "2026-03-01T08:00:00.000Z"
updated_at: "2026-03-01T08:00:00.000Z"
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
/webhooks/{id}:
parameters:
- name: id
in: path
required: true
description: UUID of the webhook subscription.
schema:
type: string
format: uuid
example: "wh-abcd-1234-5678-ef01"
get:
operationId: getWebhookSubscription
tags:
- Webhook Subscriptions
summary: Get a webhook subscription by ID
description: |
Returns a single webhook subscription record.
Requires `webhooks:read` scope.
responses:
'200':
description: Webhook subscription returned successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookSubscription'
example:
id: "wh-abcd-1234-5678-ef01"
organization_id: "org-1234-5678-abcd-ef01"
name: "Agent lifecycle events"
url: "https://my-app.example.com/webhooks/sentryagent"
events:
- agent.created
- agent.decommissioned
active: true
failure_count: 0
created_at: "2026-03-01T08:00:00.000Z"
updated_at: "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: updateWebhookSubscription
tags:
- Webhook Subscriptions
summary: Update a webhook subscription
description: |
Partially updates a webhook subscription. Only provided fields are updated.
Set `active: false` to pause delivery without deleting the subscription.
Requires `webhooks:write` scope.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateWebhookRequest'
example:
active: false
responses:
'200':
description: Webhook subscription updated successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookSubscription'
example:
id: "wh-abcd-1234-5678-ef01"
organization_id: "org-1234-5678-abcd-ef01"
name: "Agent lifecycle events"
url: "https://my-app.example.com/webhooks/sentryagent"
events:
- agent.created
- agent.decommissioned
active: false
failure_count: 0
created_at: "2026-03-01T08:00:00.000Z"
updated_at: "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."
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
delete:
operationId: deleteWebhookSubscription
tags:
- Webhook Subscriptions
summary: Delete a webhook subscription
description: |
Permanently deletes a webhook subscription and stops all future deliveries.
Any pending deliveries in the queue are cancelled.
Requires `webhooks:write` scope.
responses:
'204':
description: Webhook subscription deleted successfully. No response body.
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/webhooks/{id}/deliveries:
parameters:
- name: id
in: path
required: true
description: UUID of the webhook subscription.
schema:
type: string
format: uuid
example: "wh-abcd-1234-5678-ef01"
get:
operationId: listWebhookDeliveries
tags:
- Webhook Deliveries
summary: List delivery history for a subscription
description: |
Returns the delivery history for a webhook subscription,
ordered by `created_at` descending (most recent first).
Use this endpoint to diagnose delivery failures and inspect
payload content for historical events.
Requires `webhooks:read` scope.
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
example: 20
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
default: 0
example: 0
- name: status
in: query
required: false
schema:
$ref: '#/components/schemas/WebhookDeliveryStatus'
description: Filter deliveries by status.
responses:
'200':
description: Delivery history returned successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedDeliveriesResponse'
example:
deliveries:
- id: "del-abcd-1234-5678-ef01"
subscription_id: "wh-abcd-1234-5678-ef01"
event_type: "agent.created"
payload:
id: "evt-1234-5678"
event: "agent.created"
timestamp: "2026-04-07T09:00:00.000Z"
organization_id: "org-1234-5678-abcd-ef01"
data:
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
status: "delivered"
http_status_code: 200
attempt_count: 1
next_retry_at: null
delivered_at: "2026-04-07T09:00:05.000Z"
created_at: "2026-04-07T09:00:00.000Z"
updated_at: "2026-04-07T09:00:05.000Z"
total: 87
limit: 20
offset: 0
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'