feat: Phase 1 MVP — complete AgentIdP implementation
Implements all P0 features per OpenSpec change phase-1-mvp-implementation: - Agent Registry Service (CRUD) — full lifecycle management - OAuth 2.0 Token Service (Client Credentials flow) - Credential Management (generate, rotate, revoke) - Immutable Audit Log Service Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+ Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types Quality: 18 unit test suites, 244 tests passing, 97%+ coverage OpenAPI: 4 complete specs (14 endpoints total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
586
docs/openapi/oauth2-token.yaml
Normal file
586
docs/openapi/oauth2-token.yaml
Normal file
@@ -0,0 +1,586 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: SentryAgent.ai — OAuth 2.0 Token Service
|
||||
version: 1.0.0
|
||||
description: |
|
||||
The OAuth 2.0 Token Service provides agent authentication via the
|
||||
**Client Credentials grant** (RFC 6749 Section 4.4). It issues signed JWT
|
||||
access tokens, supports token introspection (RFC 7662), and token revocation
|
||||
(RFC 7009).
|
||||
|
||||
Agents authenticate using their `client_id` (= `agentId`) and
|
||||
`client_secret` obtained during credential provisioning.
|
||||
|
||||
**Supported Grant Type**: `client_credentials` only. All other grant types
|
||||
are rejected with `unsupported_grant_type`.
|
||||
|
||||
**Token Lifetime**: 3600 seconds (1 hour) by default.
|
||||
|
||||
**Scopes**:
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| `agents:read` | Read agent identity records |
|
||||
| `agents:write` | Create, update, and deactivate agent records |
|
||||
| `tokens:read` | Introspect tokens |
|
||||
| `audit:read` | Query the audit log |
|
||||
|
||||
**Rate Limit**: 100 requests/minute per `client_id`.
|
||||
|
||||
**Free Tier**: 10,000 token requests per month.
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api/v1
|
||||
description: Local development server
|
||||
- url: https://api.sentryagent.ai/v1
|
||||
description: Production server
|
||||
|
||||
tags:
|
||||
- name: OAuth 2.0 Tokens
|
||||
description: Token issuance, introspection, and revocation
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT access token obtained via `POST /token`.
|
||||
Required for `/token/introspect` and `/token/revoke`.
|
||||
|
||||
BasicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
HTTP Basic authentication using `client_id` as the username and
|
||||
`client_secret` as the password. Used as an alternative credential
|
||||
method for token endpoint requests (in addition to request body).
|
||||
|
||||
schemas:
|
||||
GrantType:
|
||||
type: string
|
||||
description: OAuth 2.0 grant type. Only `client_credentials` is supported.
|
||||
enum:
|
||||
- client_credentials
|
||||
example: client_credentials
|
||||
|
||||
Scope:
|
||||
type: string
|
||||
description: |
|
||||
Space-separated list of requested OAuth 2.0 scopes.
|
||||
Available scopes: `agents:read`, `agents:write`, `tokens:read`, `audit:read`.
|
||||
pattern: '^(agents:read|agents:write|tokens:read|audit:read)(\s(agents:read|agents:write|tokens:read|audit:read))*$'
|
||||
example: "agents:read agents:write"
|
||||
|
||||
TokenRequest:
|
||||
type: object
|
||||
description: |
|
||||
OAuth 2.0 Client Credentials token request body.
|
||||
Credentials may be provided in the request body (as `client_id` +
|
||||
`client_secret`) or via HTTP Basic authentication header.
|
||||
required:
|
||||
- grant_type
|
||||
properties:
|
||||
grant_type:
|
||||
$ref: '#/components/schemas/GrantType'
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: >
|
||||
The agent's `agentId` (UUID). Required if not using HTTP Basic auth.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_secret:
|
||||
type: string
|
||||
description: >
|
||||
The agent's client secret. Required if not using HTTP Basic auth.
|
||||
Treated as a sensitive value — never logged or stored in plain text.
|
||||
format: password
|
||||
example: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
|
||||
scope:
|
||||
$ref: '#/components/schemas/Scope'
|
||||
|
||||
TokenResponse:
|
||||
type: object
|
||||
description: Successful OAuth 2.0 token response.
|
||||
required:
|
||||
- access_token
|
||||
- token_type
|
||||
- expires_in
|
||||
- scope
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: >
|
||||
Signed JWT access token. Include this value in the `Authorization`
|
||||
header as `Bearer <access_token>` when calling other API endpoints.
|
||||
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type. Always `Bearer`.
|
||||
enum:
|
||||
- Bearer
|
||||
example: "Bearer"
|
||||
expires_in:
|
||||
type: integer
|
||||
description: Token lifetime in seconds from the time of issuance. Default is `3600`.
|
||||
example: 3600
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of scopes granted by this token.
|
||||
example: "agents:read agents:write"
|
||||
|
||||
OAuth2ErrorResponse:
|
||||
type: object
|
||||
description: |
|
||||
OAuth 2.0 error response as defined in RFC 6749 Section 5.2.
|
||||
Used exclusively for token endpoint errors.
|
||||
required:
|
||||
- error
|
||||
- error_description
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: >
|
||||
Machine-readable OAuth 2.0 error code.
|
||||
enum:
|
||||
- invalid_request
|
||||
- invalid_client
|
||||
- invalid_grant
|
||||
- unauthorized_client
|
||||
- unsupported_grant_type
|
||||
- invalid_scope
|
||||
example: "invalid_client"
|
||||
error_description:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "Client authentication failed. Invalid client_id or client_secret."
|
||||
|
||||
IntrospectRequest:
|
||||
type: object
|
||||
description: Token introspection request (RFC 7662).
|
||||
required:
|
||||
- token
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The token to introspect.
|
||||
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint:
|
||||
type: string
|
||||
description: >
|
||||
Optional hint about the type of token being introspected.
|
||||
Currently only `access_token` is supported.
|
||||
enum:
|
||||
- access_token
|
||||
example: "access_token"
|
||||
|
||||
IntrospectResponse:
|
||||
type: object
|
||||
description: |
|
||||
Token introspection response (RFC 7662).
|
||||
When `active` is `false`, no other fields are guaranteed to be present.
|
||||
required:
|
||||
- active
|
||||
properties:
|
||||
active:
|
||||
type: boolean
|
||||
description: >
|
||||
Whether the token is currently active (valid, not expired, not revoked).
|
||||
example: true
|
||||
sub:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Subject — the `agentId` the token was issued for.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The `client_id` (agentId) that requested the token.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of scopes granted by this token.
|
||||
example: "agents:read agents:write"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type. Always `Bearer` for active tokens.
|
||||
example: "Bearer"
|
||||
iat:
|
||||
type: integer
|
||||
description: Unix timestamp (seconds) when the token was issued.
|
||||
example: 1743151200
|
||||
exp:
|
||||
type: integer
|
||||
description: Unix timestamp (seconds) when the token expires.
|
||||
example: 1743154800
|
||||
|
||||
RevokeRequest:
|
||||
type: object
|
||||
description: Token revocation request (RFC 7009).
|
||||
required:
|
||||
- token
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The token to revoke.
|
||||
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint:
|
||||
type: string
|
||||
description: Optional hint about the token type.
|
||||
enum:
|
||||
- access_token
|
||||
example: "access_token"
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response envelope used across all SentryAgent.ai APIs.
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Machine-readable error code.
|
||||
example: "UNAUTHORIZED"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "A valid Bearer token is required."
|
||||
details:
|
||||
type: object
|
||||
description: Optional structured details providing additional context.
|
||||
additionalProperties: true
|
||||
example: {}
|
||||
|
||||
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."
|
||||
|
||||
TooManyRequests:
|
||||
description: Rate limit exceeded. Retry after the reset time.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
description: Maximum requests allowed per minute.
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
description: Requests remaining in the current window.
|
||||
example: 0
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
description: Unix timestamp when the rate limit window resets.
|
||||
example: 1743155400
|
||||
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."
|
||||
|
||||
paths:
|
||||
/token:
|
||||
post:
|
||||
operationId: issueToken
|
||||
tags:
|
||||
- OAuth 2.0 Tokens
|
||||
summary: Issue an access token (Client Credentials)
|
||||
description: |
|
||||
Issues a signed JWT access token for an agent using the OAuth 2.0
|
||||
**Client Credentials grant** (RFC 6749 §4.4).
|
||||
|
||||
The agent authenticates by providing its `client_id` (agentId) and
|
||||
`client_secret`. Credentials may be passed either:
|
||||
- In the **request body** (`client_id` + `client_secret` fields), or
|
||||
- Via **HTTP Basic authentication** header (username = `client_id`, password = `client_secret`).
|
||||
|
||||
The token is a signed JWT containing the agent's identity claims.
|
||||
Use it as a `Bearer` token on subsequent API calls.
|
||||
|
||||
**Free Tier Limit**: 10,000 token requests per month. Exceeding this
|
||||
returns `403` with `FREE_TIER_LIMIT_EXCEEDED`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRequest'
|
||||
example:
|
||||
grant_type: client_credentials
|
||||
client_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_secret: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
|
||||
scope: "agents:read agents:write"
|
||||
responses:
|
||||
'200':
|
||||
description: Access token issued successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 99
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
description: Token responses must not be cached.
|
||||
example: "no-store"
|
||||
Pragma:
|
||||
schema:
|
||||
type: string
|
||||
example: "no-cache"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenResponse'
|
||||
example:
|
||||
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
|
||||
token_type: "Bearer"
|
||||
expires_in: 3600
|
||||
scope: "agents:read agents:write"
|
||||
'400':
|
||||
description: Malformed or missing required request parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth2ErrorResponse'
|
||||
examples:
|
||||
missingGrantType:
|
||||
summary: Missing grant_type
|
||||
value:
|
||||
error: "invalid_request"
|
||||
error_description: "The 'grant_type' parameter is required."
|
||||
invalidScope:
|
||||
summary: Invalid scope requested
|
||||
value:
|
||||
error: "invalid_scope"
|
||||
error_description: "Requested scope 'admin:all' is not available."
|
||||
unsupportedGrantType:
|
||||
summary: Unsupported grant type
|
||||
value:
|
||||
error: "unsupported_grant_type"
|
||||
error_description: "Only 'client_credentials' grant type is supported."
|
||||
'401':
|
||||
description: Client authentication failed. Invalid `client_id` or `client_secret`.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth2ErrorResponse'
|
||||
example:
|
||||
error: "invalid_client"
|
||||
error_description: "Client authentication failed. Invalid client_id or client_secret."
|
||||
'403':
|
||||
description: >
|
||||
Client is not authorised to request a token. May indicate the agent
|
||||
is suspended, decommissioned, or the free tier monthly limit has been reached.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth2ErrorResponse'
|
||||
examples:
|
||||
agentSuspended:
|
||||
summary: Agent is suspended
|
||||
value:
|
||||
error: "unauthorized_client"
|
||||
error_description: "Agent is currently suspended and cannot obtain tokens."
|
||||
freeTierLimit:
|
||||
summary: Monthly token limit reached
|
||||
value:
|
||||
error: "unauthorized_client"
|
||||
error_description: "Free tier monthly token limit of 10,000 requests has been reached."
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/token/introspect:
|
||||
post:
|
||||
operationId: introspectToken
|
||||
tags:
|
||||
- OAuth 2.0 Tokens
|
||||
summary: Introspect a token (RFC 7662)
|
||||
description: |
|
||||
Determines whether a given access token is currently active (valid,
|
||||
not expired, not revoked). Returns the token's metadata if active.
|
||||
|
||||
Compliant with RFC 7662 (OAuth 2.0 Token Introspection).
|
||||
|
||||
The caller must present a valid Bearer token with `tokens:read` scope
|
||||
to use this endpoint.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntrospectRequest'
|
||||
example:
|
||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint: "access_token"
|
||||
responses:
|
||||
'200':
|
||||
description: |
|
||||
Token introspection result. Note: a `200` response is returned even
|
||||
for inactive tokens — check the `active` field to determine token validity.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 98
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntrospectResponse'
|
||||
examples:
|
||||
activeToken:
|
||||
summary: Active token
|
||||
value:
|
||||
active: true
|
||||
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
scope: "agents:read agents:write"
|
||||
token_type: "Bearer"
|
||||
iat: 1743151200
|
||||
exp: 1743154800
|
||||
inactiveToken:
|
||||
summary: Inactive (expired or revoked) token
|
||||
value:
|
||||
active: false
|
||||
'400':
|
||||
description: Missing or malformed `token` parameter.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "The 'token' parameter is required."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Caller's token does not have the `tokens:read` scope.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INSUFFICIENT_SCOPE"
|
||||
message: "The 'tokens:read' scope is required to introspect tokens."
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/token/revoke:
|
||||
post:
|
||||
operationId: revokeToken
|
||||
tags:
|
||||
- OAuth 2.0 Tokens
|
||||
summary: Revoke a token (RFC 7009)
|
||||
description: |
|
||||
Revokes an access token, immediately invalidating it for all subsequent
|
||||
requests. Compliant with RFC 7009 (OAuth 2.0 Token Revocation).
|
||||
|
||||
Revoking an already-revoked or expired token is considered a success
|
||||
(idempotent operation per RFC 7009 §2.1).
|
||||
|
||||
The caller must present a valid Bearer token to revoke another token.
|
||||
An agent may revoke its own tokens; admin scope is required to revoke
|
||||
tokens belonging to other agents.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RevokeRequest'
|
||||
example:
|
||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint: "access_token"
|
||||
responses:
|
||||
'200':
|
||||
description: |
|
||||
Token revoked successfully (or was already inactive).
|
||||
Per RFC 7009, revocation always returns `200` for any valid request,
|
||||
even if the token was already revoked or expired.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 97
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
example: {}
|
||||
'400':
|
||||
description: Missing or malformed `token` parameter.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "The 'token' parameter is required."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Insufficient permissions to revoke this token.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to revoke this token."
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
Reference in New Issue
Block a user