From 36d8714d2c3be2aa867003a3e63d02de15663fe1 Mon Sep 17 00:00:00 2001
From: James Hateley
Date: Thu, 23 Jul 2026 13:13:11 +1000
Subject: [PATCH 1/4] feat(ref-imp): use ans repo spec to generate models
Signed-off-by: James Hateley
---
.../sdk/agent/connection/AgentConnection.java | 10 +-
.../agent/connection/AgentConnectionTest.java | 10 +-
ans-sdk-api/build.gradle.kts | 26 +-
ans-sdk-api/spec/api-spec.yaml | 3159 +++++++++++++++++
.../sdk/discovery/AgentCapabilityRequest.java | 32 +
.../ans/sdk/discovery/ResolutionService.java | 1 -
.../sdk/discovery/DiscoveryClientTest.java | 12 +-
.../sdk/registration/RegistrationClient.java | 5 +-
.../registration/RegistrationClientTest.java | 36 +-
9 files changed, 3234 insertions(+), 57 deletions(-)
create mode 100644 ans-sdk-api/spec/api-spec.yaml
create mode 100644 ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java
diff --git a/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/connection/AgentConnection.java b/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/connection/AgentConnection.java
index b3a8da3..4c71332 100644
--- a/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/connection/AgentConnection.java
+++ b/ans-sdk-agent-client/src/main/java/com/godaddy/ans/sdk/agent/connection/AgentConnection.java
@@ -5,6 +5,7 @@
import com.godaddy.ans.sdk.agent.protocol.HttpApiClient;
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import com.godaddy.ans.sdk.model.generated.AgentEndpoint;
+import com.godaddy.ans.sdk.model.generated.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -178,13 +179,14 @@ public Optional getMetadataUrl(String protocol) {
/**
* Checks if a Protocol enum matches the protocol string.
*/
- private boolean matchesProtocol(AgentEndpoint.ProtocolEnum endpointProtocol, String protocolStr) {
+ private boolean matchesProtocol(Protocol endpointProtocol, String protocolStr) {
if (endpointProtocol == null || protocolStr == null) {
return false;
}
- // Handle both enum name (HTTP_API) and value (HTTP-API)
- return protocolStr.equals(endpointProtocol.name())
- || protocolStr.equals(endpointProtocol.getValue());
+ // Normalize hyphens to underscores so "HTTP-API" form matches HTTP_API
+ String normalized = protocolStr.replace('-', '_');
+ return normalized.equals(endpointProtocol.name())
+ || normalized.equals(endpointProtocol.getValue());
}
/**
diff --git a/ans-sdk-agent-client/src/test/java/com/godaddy/ans/sdk/agent/connection/AgentConnectionTest.java b/ans-sdk-agent-client/src/test/java/com/godaddy/ans/sdk/agent/connection/AgentConnectionTest.java
index 52186d0..7c6375a 100644
--- a/ans-sdk-agent-client/src/test/java/com/godaddy/ans/sdk/agent/connection/AgentConnectionTest.java
+++ b/ans-sdk-agent-client/src/test/java/com/godaddy/ans/sdk/agent/connection/AgentConnectionTest.java
@@ -5,6 +5,7 @@
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import com.godaddy.ans.sdk.model.generated.AgentEndpoint;
import com.godaddy.ans.sdk.model.generated.AgentLifecycleStatus;
+import com.godaddy.ans.sdk.model.generated.Protocol;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -16,6 +17,7 @@
import java.time.Duration;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -36,16 +38,16 @@ class AgentConnectionTest {
void setUp() {
// Create test agent details with endpoints
AgentEndpoint httpApiEndpoint = new AgentEndpoint();
- httpApiEndpoint.setProtocol(AgentEndpoint.ProtocolEnum.HTTP_API);
+ httpApiEndpoint.setProtocol(Protocol.HTTP_API);
httpApiEndpoint.setAgentUrl(URI.create("https://agent.example.com/api"));
httpApiEndpoint.setMetaDataUrl(URI.create("https://agent.example.com/metadata"));
AgentEndpoint a2aEndpoint = new AgentEndpoint();
- a2aEndpoint.setProtocol(AgentEndpoint.ProtocolEnum.A2_A);
+ a2aEndpoint.setProtocol(Protocol.A2_A);
a2aEndpoint.setAgentUrl(URI.create("wss://agent.example.com/a2a"));
agentDetails = new AgentDetails();
- agentDetails.setAgentId("test-agent-id");
+ agentDetails.setAgentId(UUID.randomUUID());
agentDetails.setAnsName("ans://v1.0.0.agent.example.com");
agentDetails.setAgentHost("agent.example.com");
agentDetails.setVersion("1.0.0");
@@ -94,7 +96,7 @@ void supportsProtocolWithProtocolValueShouldReturnTrue() {
// Given
AgentConnection connection = new AgentConnection(agentDetails, ansHttpClient);
- // When/Then - using protocol value format (HTTP-API)
+ // When/Then - legacy hyphenated alias (HTTP-API) should still match HTTP_API
assertThat(connection.supportsProtocol("HTTP-API")).isTrue();
}
diff --git a/ans-sdk-api/build.gradle.kts b/ans-sdk-api/build.gradle.kts
index 8a4199f..24c110b 100644
--- a/ans-sdk-api/build.gradle.kts
+++ b/ans-sdk-api/build.gradle.kts
@@ -7,8 +7,7 @@ plugins {
val jacksonVersion: String by project
// Authoritative source for the API spec
-val apiSpecUrl = "https://developer.godaddy.com/swagger/swagger_ans.json"
-val apiSpecFile = layout.buildDirectory.file("api-spec.json")
+val apiSpecFile = layout.projectDirectory.file("spec/api-spec.yaml")
dependencies {
// Jackson for JSON serialization
@@ -20,25 +19,10 @@ dependencies {
implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
}
-// Task to download the API spec from the authoritative source
-val downloadApiSpec by tasks.registering {
- val outputFile = apiSpecFile
- outputs.file(outputFile)
- doLast {
- val destFile = outputFile.get().asFile
- destFile.parentFile.mkdirs()
- URI.create(apiSpecUrl).toURL().openStream().use { input ->
- destFile.outputStream().use { output ->
- input.copyTo(output)
- }
- }
- logger.lifecycle("Downloaded API spec from $apiSpecUrl")
- }
-}
-
openApiGenerate {
generatorName.set("java")
- inputSpec.set(apiSpecFile.get().asFile.absolutePath)
+ inputSpec.set(apiSpecFile)
+ skipValidateSpec.set(true)
outputDir.set(layout.buildDirectory.dir("generated").get().asFile.absolutePath)
apiPackage.set("com.godaddy.ans.sdk.api.generated")
modelPackage.set("com.godaddy.ans.sdk.model.generated")
@@ -61,10 +45,6 @@ sourceSets {
}
}
-tasks.openApiGenerate {
- dependsOn(downloadApiSpec)
-}
-
tasks.compileJava {
dependsOn(tasks.openApiGenerate)
}
diff --git a/ans-sdk-api/spec/api-spec.yaml b/ans-sdk-api/spec/api-spec.yaml
new file mode 100644
index 0000000..770e4ed
--- /dev/null
+++ b/ans-sdk-api/spec/api-spec.yaml
@@ -0,0 +1,3159 @@
+openapi: 3.0.3
+info:
+ title: Agent Name Service (ANS) Management API
+ description: |
+ Management API for the Agent Name Service (ANS) — v2.
+
+ This API provides authenticated, ownership-scoped operations for managing
+ ANS agent registrations, certificates, and lifecycle. It replaces the
+ /v1/agents API with the following key changes:
+
+ **Route structure:** All routes moved from `/v1/agents` to `/v2/ans/agents`.
+ Registration uses `POST /v2/ans/agents` (standard REST collection POST,
+ no `/register` verb suffix). Action endpoints (`/revoke`, `/verify-acme`,
+ `/verify-dns`) retain verb suffixes for procedural clarity.
+
+ **Management-scoped listing:** `GET /v2/ans/agents` returns only agents owned by
+ the authenticated caller. Global search/discovery has moved to a dedicated
+ search service.
+
+ **Cursor-based pagination:** The list endpoint uses cursor-based pagination
+ instead of offset-based, eliminating the O(n) performance degradation at
+ high offsets.
+
+ **Ownership enforcement:** All agent-specific operations (`GET /{agentId}`,
+ `POST /{agentId}/revoke`, certificate endpoints) enforce that the
+ authenticated caller owns the requested agent.
+
+ **Resolution removed:** `POST /v1/agents/resolution` is not carried forward
+ to v2. The `_ans` DNS TXT record already contains agent endpoints, making
+ this endpoint redundant. Consumers needing discovery should use the
+ registered agents search service (`/v1/ans/registered-agents`).
+
+ ## Deprecation of v1
+
+ All `/v1/agents/*` routes are deprecated and will be removed per the
+ published sunset schedule. v1 responses include `Deprecation: true`,
+ `Sunset`, and `Link` headers pointing to v2 equivalents.
+ version: 2.0.0
+ contact:
+ name: ANS Working Group
+ license:
+ name: Apache 2.0
+ url: https://www.apache.org/licenses/LICENSE-2.0.html
+
+servers:
+ - url: http://localhost:18080/v2
+ description: Local dev — quickstart ra-local.yaml
+ - url: https://api.ans.example.org/v2
+ description: Main ANS API server (v2)
+ - url: https://test.ans.example.org/v2
+ description: Test server (v2)
+
+tags:
+ - name: Management
+ description: Agent listing and detail operations (ownership-scoped)
+ - name: Registration
+ description: Agent registration and validation operations
+ - name: Revocation
+ description: Agent certificate revocation operations
+ - name: Certificate Management
+ description: Certificate retrieval, CSR submission, and renewal operations
+ - name: Events
+ description: |
+ Public, unauthenticated agent-lifecycle events feed. Lives on the
+ `/v1` path (not `/v2`) because it is the production-compatible
+ ingestion contract the ANS Finder consumes; its shape is pinned to
+ the production `getAgentEvents` operation. The operation declares a
+ path-level `servers` override so the documented path resolves at
+ the host root rather than under this document's `/v2` server prefix.
+ - name: Verified Identities
+ description: |
+ The "who" behind agents — owner-level identities (did:web,
+ did:key) proven through challenge-bound control proofs, sealed
+ on their own Transparency Log stream, and linked to the owner's
+ agents
+
+# Global security requirement. Every path inherits this and can
+# override with `security: []` to declare itself anonymous. The
+# handler's actual auth behaviour is documented in CLAUDE.md §Auth:
+# static API key (Bearer ) for quickstart, OIDC bearer tokens
+# when `auth.type: oidc` is configured. Both providers accept the
+# same shape on the wire, which is why a single HTTP-bearer scheme
+# covers both in the OpenAPI document.
+security:
+ - bearerAuth: []
+
+paths:
+ # ──────────────────────────────────────────────────────────────────
+ # Events — public agent-lifecycle feed (the Finder's ingestion source)
+ #
+ # Path-level `servers` override: this operation lives at the host
+ # ROOT (`/v1/agents/events`), not under this document's `/v2` server
+ # prefix. The override makes the rendered URL correct in Swagger UI.
+ # The shape is byte-compatible with the production `getAgentEvents`
+ # operation; the OSS RA owns a conformance test asserting that.
+ # ──────────────────────────────────────────────────────────────────
+ /v1/agents/events:
+ servers:
+ - url: http://localhost:18080
+ description: Local dev — quickstart ra-local.yaml (feed is not under /v2)
+ - url: https://api.ans.example.org
+ description: Main ANS API server
+ get:
+ tags:
+ - Events
+ summary: Retrieve ANS agent lifecycle events
+ description: |
+ Returns a paginated, strictly-ordered stream of ANS agent
+ lifecycle events (registrations and revocations). This is the
+ ingestion contract the ANS Finder polls to build its discovery
+ catalog.
+
+ **Anonymous.** This route requires no authentication (`security: []`).
+
+ **Only sealed events appear.** An event is served only after it
+ has been accepted by the Transparency Log and a `logId` assigned,
+ so an item in the feed is provably sealed in the log and its
+ receipt is resolvable from `logId`.
+
+ **Pagination.** Pass the `lastLogId` from a response as the next
+ request's `lastLogId` query parameter to fetch the following
+ page. Omit it for the first page. When the response omits
+ `lastLogId`, there are no more results.
+
+ **Retention.** Events are retained for a bounded window (default
+ 30 days) and then age out. Because of this, an unknown
+ `lastLogId` and one that has aged out are indistinguishable: both
+ restart the stream from the oldest retained event rather than
+ erroring.
+
+ **providerId.** Accepted for production-shape parity. This
+ open-source RA has no provider concept, so supplying a non-empty
+ `providerId` yields an empty page.
+ operationId: getAgentEvents
+ security: []
+ parameters:
+ - name: providerId
+ in: query
+ description: |
+ Optional provider identifier. This OSS RA has no provider
+ concept; a non-empty value returns an empty page.
+ required: false
+ schema:
+ type: string
+ - name: lastLogId
+ in: query
+ description: |
+ Opaque cursor — the `lastLogId` from a previous response.
+ Omit for the first page. An unknown or aged-out cursor
+ restarts from the oldest retained event.
+ required: false
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Number of events to return (1-200, default 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 200
+ default: 100
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EventPageResponse'
+ '422':
+ description: Invalid request parameters (out-of-range or non-numeric limit)
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '500':
+ description: Internal server error
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+
+ # ──────────────────────────────────────────────────────────────────
+ # Management — ownership-scoped
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents:
+ get:
+ tags:
+ - Management
+ summary: List caller's agents
+ description: |
+ Returns agents owned by the authenticated caller. This is a management
+ endpoint, not a discovery/search endpoint.
+
+ **Key differences from v1 `GET /v1/agents`:**
+ - Results are scoped to the authenticated caller's agents only.
+ - Uses cursor-based pagination (no `offset`).
+ - Removed filters: `protocol`, `agentDisplayName`, `version`.
+ Use the dedicated search service for discovery use cases.
+ - Removed `totalCount` and `searchCriteria` from response.
+
+ **Pagination:** Use the `nextCursor` value from the response as the
+ `cursor` query parameter in the next request. When `hasMore` is false,
+ there are no more results.
+ operationId: listCallerAgents
+ parameters:
+ - name: status
+ in: query
+ description: |
+ Filter by agent lifecycle status. Can specify multiple values.
+ Defaults to ACTIVE. Use 'ALL' to return agents of any status.
+ required: false
+ style: form
+ explode: true
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/AgentLifecycleStatusFilter'
+ - name: agentHost
+ in: query
+ description: Filter by agent host domain (exact match)
+ required: false
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: |
+ Opaque pagination cursor from a previous response's `nextCursor`.
+ Omit for the first page.
+ required: false
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Agent list retrieved successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentListResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Invalid query parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ post:
+ tags:
+ - Registration
+ summary: Register a new agent with the ANS
+ description: |
+ Registers a new agent with the Agent Name Service.
+
+ The request MAY include a Certificate Signing Request (CSR) so that
+ an Identity certificate can be issued. Identity certificates are
+ issued by the RA.
+
+ The request must include a server CSR (for RA-issued server cert)
+ or a BYOC server certificate with its chain.
+ operationId: registerAgent
+ requestBody:
+ description: Agent registration request
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentRegistrationRequest'
+ responses:
+ '202':
+ description: Registration accepted, pending validation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RegistrationPending'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: |
+ Conflict. `code` distinguishes the cause:
+ `ANS_NAME_TAKEN` (this exact versioned ANS name is already
+ registered) or `AGENT_HOST_TAKEN` (the FQDN has a live —
+ ACTIVE or DEPRECATED — registration owned by a different
+ operator; the FQDN belongs to that owner until no live
+ registration remains).
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '422':
+ description: Invalid registration request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/agents/{agentId}:
+ get:
+ tags:
+ - Management
+ summary: Get agent details
+ description: |
+ Retrieves detailed information about a registered agent.
+
+ **Ownership enforced:** Returns 404 if the authenticated caller does
+ not own the requested agent.
+ operationId: getAgent
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: Agent details retrieved successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentDetails'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found or not owned by caller
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ # ──────────────────────────────────────────────────────────────────
+ # AI Catalog (producer-generated discovery artifact)
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents/{agentId}/catalog-entry:
+ get:
+ tags:
+ - Management
+ summary: Get the agent's AI Catalog entry
+ description: |
+ Returns the agent's AI Catalog `CatalogEntry` — the producer-side
+ record an external AI Catalog / ARD registry ingests. The entry is
+ derived entirely from the registration aggregate (display name,
+ version, endpoints, Transparency-Log badge/receipt URLs); nothing is
+ sealed or fetched, and it is regenerated on lifecycle events.
+
+ **Owner-scoped, read-only.** Returns 404 if the authenticated caller
+ does not own the agent (existence is hidden). This route is the
+ registrant's own view of how their agent appears in a catalog; it is
+ not the public, crawlable surface.
+
+ **Eligibility:** an entry is produced only for an **ACTIVE**
+ registration (the entry's SCITT-receipt attestation and TL badge
+ link to Transparency-Log records that exist only once the agent is
+ sealed at activation — a pending agent has nothing to link to) that
+ is versioned and has at least one A2A or MCP endpoint whose
+ `metaDataUrl` is present and passes the emit-side URL policy
+ (absolute https, no userinfo/query/fragment, host == agentHost).
+ Otherwise the route returns 422 `NOT_CATALOG_ELIGIBLE` (the `detail`
+ names the specific gate: not active, versionless, or no eligible
+ endpoint).
+ operationId: getCatalogEntry
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: The agent's CatalogEntry
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CatalogEntry'
+ '401':
+ description: Authentication failed
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '403':
+ description: Authorization failed
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '404':
+ description: Agent not found or not owned by caller
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '422':
+ description: |
+ Registration is not catalog-eligible: not ACTIVE (no
+ Transparency-Log entry to link to yet), versionless, or no
+ A2A/MCP endpoint with a policy-passing metaDataUrl. `code` is
+ `NOT_CATALOG_ELIGIBLE`; `detail` names the specific gate.
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '500':
+ description: Internal server error
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+
+ /ans/agents/{agentId}/ai-catalog:
+ get:
+ tags:
+ - Management
+ summary: Get the host-complete AI Catalog document
+ description: |
+ Returns the host-complete AI Catalog **document** for the agent's
+ host — the file an Agent Hosting Platform publishes verbatim at
+ `/.well-known/ai-catalog.json`. It contains one `CatalogEntry` for
+ every **ACTIVE**, catalog-eligible agent registered on that host
+ (deprecated and revoked registrations are pruned from the
+ well-known file; the population export keeps them, marked).
+
+ **Owner-scoped, read-only.** Returns 404 if the authenticated
+ caller does not own the agent (existence is hidden). The caller
+ proves ownership of the keying agent; the document then lists that
+ owner's ACTIVE catalog-eligible agents on the host (the body is
+ scoped to the owner, not just the route key, so a shared host never
+ discloses one owner's agents to another). This is the registrant's
+ refresh target — not a public, crawlable surface.
+
+ **Caching:** the response carries a strong `ETag` (SHA-256 of the
+ body). A conditional request with a matching `If-None-Match`
+ receives `304 Not Modified`.
+ operationId: getHostCatalog
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: The host-complete AI Catalog document
+ headers:
+ ETag:
+ description: Strong validator (SHA-256 of the document body)
+ schema:
+ type: string
+ Cache-Control:
+ schema:
+ type: string
+ content:
+ application/ai-catalog+json:
+ schema:
+ $ref: '#/components/schemas/CatalogDocument'
+ '304':
+ description: Not modified (If-None-Match matched the current ETag)
+ headers:
+ ETag:
+ description: Strong validator (SHA-256 of the document body)
+ schema:
+ type: string
+ Cache-Control:
+ schema:
+ type: string
+ '401':
+ description: Authentication failed
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '403':
+ description: Authorization failed
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '404':
+ description: Agent not found or not owned by caller
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '500':
+ description: Internal server error
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+
+ # ──────────────────────────────────────────────────────────────────
+ # Registration Validation
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents/{agentId}/verify-acme:
+ post:
+ tags:
+ - Registration
+ summary: Trigger ACME validation
+ description: |
+ Initiates domain control validation. The AHP calls this after placing
+ the ACME challenge token. The RA validates domain control, which is
+ required before issuing both server and identity certificates.
+ operationId: validateRegistration
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '202':
+ description: Validation successful, certificates pending
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentStatus'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found or not owned by caller
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: |
+ `AGENT_HOST_TAKEN` — a different operator now holds this FQDN
+ live (it activated first). This registration lost the host and
+ was cancelled; it can no longer progress.
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '422':
+ description: Validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/agents/{agentId}/verify-dns:
+ post:
+ tags:
+ - Registration
+ summary: Verify DNS records configured
+ description: |
+ Verifies that all required DNS records have been configured correctly.
+ This is the final step for external domain registration.
+
+ **Seal-before-success:** activation does not report `ACTIVE` until
+ the agent's single terminal `AGENT_REGISTERED` event has been
+ sealed in the Transparency Log. The RA submits the event to the TL
+ INLINE and only then commits the ACTIVE transition; if the TL is
+ unavailable the call fails 503 `TL_UNAVAILABLE` (retryable — the
+ agent stays `PENDING_DNS` and nothing is committed). This is what
+ guarantees a catalog entry's SCITT-receipt and badge links can only
+ ever point at a TL record that actually exists.
+ operationId: verifyDnsRecords
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '202':
+ description: DNS verified and AGENT_REGISTERED sealed in the TL; registration active
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentStatus'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found or not owned by caller
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: |
+ `AGENT_HOST_TAKEN` — a different operator activated a
+ registration on this FQDN during this registration's pending
+ window. The FQDN belongs to that owner until no live
+ registration remains, so this activation is refused.
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ '422':
+ description: DNS records not found or incorrect
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DnsVerificationError'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '503':
+ description: |
+ `TL_UNAVAILABLE` — the Transparency Log could not seal the
+ `AGENT_REGISTERED` event. Retryable: the agent stays
+ `PENDING_DNS` and nothing is committed; retry verify-dns once
+ the TL is reachable.
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+
+ # ──────────────────────────────────────────────────────────────────
+ # Revocation
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents/{agentId}/revoke:
+ post:
+ tags:
+ - Revocation
+ summary: Revoke an active agent or cancel a pending registration
+ description: |
+ Revokes an active agent or cancels a pending registration.
+
+ **Ownership enforced:** Returns 403 if caller does not own the agent.
+
+ For ACTIVE agents: Revokes due to key compromise, decommissioning, or
+ other security reasons. Certificate added to CRL, registry entry flagged.
+
+ For PENDING registrations (PENDING_CERTS or PENDING_DNS): Cancels
+ the registration, revokes any already-issued certificates.
+
+ PENDING_VALIDATION registrations are not cancellable and will
+ auto-expire.
+ operationId: revokeAgent
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ requestBody:
+ description: Agent revocation request
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentRevocationRequest'
+ responses:
+ '200':
+ description: Agent revoked successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentRevocationResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed or caller does not own agent
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Invalid revocation request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ # ──────────────────────────────────────────────────────────────────
+ # Certificate Management — Server
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents/{agentId}/certificates/server:
+ get:
+ tags:
+ - Certificate Management
+ summary: Get agent's server certificates
+ description: Retrieves all TLS server certificates for the specified agent.
+ operationId: getAgentServerCertificates
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: Server certificate(s) retrieved
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/CertificateResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent or certificate not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ post:
+ tags:
+ - Certificate Management
+ summary: Submit server certificate CSR
+ description: |
+ Submits a CSR for the agent's server certificate.
+ The response contains a `csrId` matching `CertificateResponse.csrId`.
+ operationId: submitServerCsr
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CsrSubmissionRequest'
+ responses:
+ '202':
+ description: CSR accepted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CsrSubmissionResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Invalid CSR
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/agents/{agentId}/certificates/server/renewal:
+ post:
+ tags:
+ - Certificate Management
+ summary: Submit server certificate renewal
+ description: |
+ Initiates server certificate renewal. Supports CSR path (RA issues)
+ or BYOC path (client provides certificate). Only one pending renewal
+ allowed per agent.
+ operationId: submitServerCertRenewal
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ServerCertificateRenewalRequest'
+ responses:
+ '202':
+ description: Renewal accepted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RenewalSubmissionResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: Pending renewal already exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ get:
+ tags:
+ - Certificate Management
+ summary: Get pending renewal status
+ description: Returns current renewal status if one exists.
+ operationId: getServerCertRenewalStatus
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: Renewal found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RenewalStatusResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: No renewal exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ delete:
+ tags:
+ - Certificate Management
+ summary: Cancel pending renewal
+ description: |
+ Cancels the pending server certificate renewal. Marks associated CSR
+ as REJECTED if applicable. Client can immediately submit a new renewal.
+ operationId: cancelServerCertRenewal
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '204':
+ description: Renewal cancelled
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: No pending renewal
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Renewal already completed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/agents/{agentId}/certificates/server/renewal/verify-acme:
+ post:
+ tags:
+ - Certificate Management
+ summary: Verify ACME challenges for pending server cert renewal
+ description: |
+ Triggers ACME validation for external agents only.
+ DNS-01 or HTTP-01 challenge verification.
+ operationId: verifyRenewalAcme
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: BYOC renewal completed synchronously
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RenewalVerificationResponse'
+ '202':
+ description: CSR renewal validated, certificate issuance in progress
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RenewalVerificationResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: No pending renewal found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ # ──────────────────────────────────────────────────────────────────
+ # Certificate Management — Identity
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents/{agentId}/certificates/identity:
+ get:
+ tags:
+ - Certificate Management
+ summary: Get agent's identity certificates
+ description: Retrieves all identity certificates for the specified agent.
+ operationId: getAgentIdentityCertificates
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '200':
+ description: Identity certificate(s) retrieved
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/CertificateResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent or certificate not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ post:
+ tags:
+ - Certificate Management
+ summary: Submit identity certificate CSR
+ description: Submits a CSR for the agent's identity certificate.
+ operationId: submitIdentityCsr
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CsrSubmissionRequest'
+ responses:
+ '202':
+ description: CSR accepted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CsrSubmissionResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Agent not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Invalid CSR
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ # ──────────────────────────────────────────────────────────────────
+ # CSR Status
+ # ──────────────────────────────────────────────────────────────────
+ /ans/agents/{agentId}/csrs/{csrId}/status:
+ get:
+ tags:
+ - Certificate Management
+ summary: Get CSR status
+ description: Retrieves the current status of a Certificate Signing Request.
+ operationId: getCsrStatus
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ - name: csrId
+ in: path
+ description: Unique identifier of the CSR
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ '200':
+ description: CSR status retrieved
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CsrStatusResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Authorization failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: CSR not found or does not belong to this agent
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Invalid request parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ # ──────────────────────────────────────────────────────────────────
+ # Verified Identities — the "who" behind agents
+ #
+ # An identity is a first-class object owned by the authenticated
+ # principal (the same owner as the agents), proven through a
+ # per-kind control proof, sealed onto its own Transparency Log
+ # stream, and linked to any number of that owner's agents. The
+ # agent registration surface above is unchanged — agents carry no
+ # identity fields; the association is the links sub-resource.
+ #
+ # The single security invariant: the RA seals an identity
+ # attestation only after control is PROVEN — a challenge-bound
+ # signature verified against the identifier's authoritative keys —
+ # never on resolution alone.
+ # ──────────────────────────────────────────────────────────────────
+ /ans/identities:
+ post:
+ tags:
+ - Verified Identities
+ summary: Register a verified identity
+ description: |
+ Registers an identifier (the kind — `did:web`, `did:key` — is
+ inferred from the value's lexical form, never caller-asserted)
+ and returns the challenge round to sign: one entry per
+ eligible key, all over a single anti-replay nonce. The
+ identity is created in `PENDING_CONTROL`; nothing is sealed
+ until verify-control passes.
+
+ Re-POSTing the same value while the row is `PENDING_CONTROL`
+ is the idempotent re-add: the same `identityId` returns with
+ a fresh nonce (the prior nonce is superseded). A value
+ already verified — by this owner or any other — returns 409
+ `IDENTIFIER_DUPLICATE`. A recognized value whose kind has no
+ enabled control verifier (e.g. `did:plc`, `did:ion`, until
+ their verifiers ship) returns 422
+ `IDENTIFIER_KIND_UNSUPPORTED`. The `lei` kind additionally
+ requires a `vleiPresentation` at register time; omitting it
+ returns 422 `IDENTIFIER_PRESENTATION_REQUIRED`.
+ operationId: registerIdentity
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityRegistrationRequest'
+ responses:
+ '202':
+ description: Identity registered — challenges to sign
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityChallengeResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: Identifier already verified (IDENTIFIER_DUPLICATE)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: |
+ Invalid or unsupported identifier; for the `did:web` kind the
+ register-time resolution may fail with `DID_RESOLUTION_FAILED`
+ or `DID_DOCUMENT_ID_MISMATCH`; for the `lei` kind —
+ `IDENTIFIER_PRESENTATION_REQUIRED`, `LEI_PRESENTATION_INVALID`,
+ `LEI_MISMATCH`, `LEI_SUBJECT_AID_INVALID`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '503':
+ description: |
+ Upstream unavailable — `LEI_VERIFIER_UNAVAILABLE` (the `lei`
+ verifier runs synchronously at register; retryable).
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ get:
+ tags:
+ - Verified Identities
+ summary: List my identities
+ description: |
+ Returns one page of the caller's identities, newest first, in
+ the same limit + opaque-cursor envelope as
+ `GET /v2/ans/agents` (`AgentListResponse`): the collection
+ array is named `items`, alongside `returnedCount`, `limit`,
+ `nextCursor`, and `hasMore`. Pass `nextCursor` back as
+ `cursor` for the next page; a null `nextCursor` is the last
+ page.
+ operationId: listIdentities
+ parameters:
+ - name: limit
+ in: query
+ required: false
+ schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
+ - name: cursor
+ in: query
+ required: false
+ schema: { type: string }
+ description: Opaque cursor from the previous page's nextCursor
+ responses:
+ '200':
+ description: One page of the caller's identities
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityListResponse'
+ '401':
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/identities/{identityId}:
+ get:
+ tags:
+ - Verified Identities
+ summary: Get identity detail
+ description: |
+ Returns the identity plus its live links. Ownership-scoped:
+ an identity that doesn't exist or isn't the caller's returns
+ 404 (existence is hidden).
+ operationId: getIdentity
+ parameters:
+ - $ref: '#/components/parameters/IdentityIdPath'
+ responses:
+ '200':
+ description: Identity detail
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityDetails'
+ '404':
+ description: Identity not found or not accessible
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ put:
+ tags:
+ - Verified Identities
+ summary: Rotate / replace the identifier
+ description: |
+ Stages a same-kind replacement and returns fresh challenges
+ over it. Until the new proof lands the previously sealed
+ state stands; a replacement that never verifies expires with
+ its nonce. On a clean verify-control the RA swaps the value
+ and seals ONE `IDENTITY_UPDATED` event — regardless of how
+ many agents are linked. Cross-kind replacement is rejected
+ (revoke and register a new identity instead).
+ operationId: rotateIdentity
+ parameters:
+ - $ref: '#/components/parameters/IdentityIdPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityRegistrationRequest'
+ responses:
+ '202':
+ description: Rotation staged — fresh challenges to sign
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityChallengeResponse'
+ '403':
+ description: Caller does not own this identity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Identity not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: Identity is not in a rotatable state
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: |
+ Invalid replacement value (incl. `IDENTIFIER_KIND_MISMATCH`);
+ for the `did:web` kind the register-time resolution may fail
+ with `DID_RESOLUTION_FAILED` or `DID_DOCUMENT_ID_MISMATCH`;
+ for the `lei` kind — `IDENTIFIER_PRESENTATION_REQUIRED`,
+ `LEI_PRESENTATION_INVALID`, `LEI_MISMATCH`,
+ `LEI_SUBJECT_AID_INVALID`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '503':
+ description: |
+ Upstream unavailable — `LEI_VERIFIER_UNAVAILABLE` (the `lei`
+ verifier runs synchronously at rotate; retryable).
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/identities/{identityId}/verify-control:
+ post:
+ tags:
+ - Verified Identities
+ summary: Prove control of the identifier
+ description: |
+ Submits the control proofs: one compact JWS per proven key,
+ each payload equal — verbatim — to the served `signingInput`
+ (clients never canonicalize; the RA checks payload equality
+ before verifying any signature). Every proof must verify
+ against the identifier's AUTHORITATIVE key for its `kid`
+ (resolved from the DID document for `did:web`, decoded from
+ the identifier for `did:key`); one bad proof fails the call
+ closed. The nonce is consumed exactly once, inside the
+ success transaction — a failed attempt does not consume it.
+
+ The `lei` kind instead submits a single `cesrSignature` over
+ the same `signingInput`: the RA forwards it to the
+ vlei-verifier, which live-rechecks the subject AID's
+ authorization and verifies the signature against its current
+ KEL key state.
+
+ JWS bearing unrecognized `crit` header parameters are
+ rejected (RFC 7515 §4.1.11) — this verifier implements no
+ critical extensions.
+
+ Seal-before-success: the RA seals `IDENTITY_VERIFIED` /
+ `IDENTITY_UPDATED` on the identity's own Transparency Log
+ stream — every proven key sealed self-verifyingly (public
+ key + signed proof) — and reports success ONLY after the TL
+ acknowledges the seal; the row transition commits with that
+ acknowledgment, so anything this API reports as set up is
+ resolvable in the TL at that moment. If the TL is
+ unavailable the call fails 503 `TL_UNAVAILABLE`, retryable:
+ the nonce is NOT consumed and the prior state stands.
+ operationId: verifyIdentityControl
+ parameters:
+ - $ref: '#/components/parameters/IdentityIdPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VerifyControlRequest'
+ responses:
+ '200':
+ description: Control proven — identity VERIFIED, event sealed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityDetails'
+ '403':
+ description: Caller does not own this identity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Identity not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: |
+ Challenge state error — `PRICC_TOKEN_EXPIRED`,
+ `PRICC_TOKEN_ALREADY_USED`, `IDENTIFIER_CHALLENGE_EXPIRED`
+ — the identity is revoked, a concurrent verify attempt
+ holds the seal claim (`VERIFICATION_IN_FLIGHT` — retry
+ shortly; the claim expires within ~30s), or the
+ identifier is already verified by another owner
+ (`IDENTIFIER_DUPLICATE`). Recovery from an expired nonce
+ is the idempotent re-add (re-POST the same value).
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: |
+ Proof rejected — `PRICC_SIGNATURE_INVALID`,
+ `DID_VERIFICATION_METHOD_INVALID`, `DID_RESOLUTION_FAILED`,
+ `DID_DOCUMENT_ID_MISMATCH`, `IDENTIFIER_PROOF_INVALID`, `LEI_NOT_AUTHORIZED`, `LEI_MISMATCH`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ '503':
+ description: Upstream unavailable (TL_UNAVAILABLE, or LEI_VERIFIER_UNAVAILABLE — retryable; the nonce is NOT consumed)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/identities/{identityId}/revoke:
+ post:
+ tags:
+ - Verified Identities
+ summary: Revoke the identity
+ description: |
+ Transitions a VERIFIED identity to REVOKED and seals ONE
+ `IDENTITY_REVOKED` event. A POST (state change), never a
+ DELETE: an identity cannot be deleted — its history is
+ append-only in the Transparency Log. Propagation to every
+ linked agent's badge is the TL's read-time join, not a write
+ fan-out.
+
+ Seal-before-success: the row flips only after the TL
+ acknowledges the seal. TL unavailable → 503 `TL_UNAVAILABLE`,
+ retryable, the identity stays VERIFIED.
+ operationId: revokeIdentity
+ parameters:
+ - $ref: '#/components/parameters/IdentityIdPath'
+ responses:
+ '200':
+ description: Identity revoked, event sealed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityDetails'
+ '403':
+ description: Caller does not own this identity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Identity not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: Identity is not VERIFIED (nothing sealed to revoke)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '503':
+ description: Transparency Log unavailable (TL_UNAVAILABLE — retryable, nothing changed)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/identities/{identityId}/links:
+ post:
+ tags:
+ - Verified Identities
+ summary: Link agents to the identity
+ description: |
+ Binds a batch of the caller's agents to the identity — a
+ single owner-gated call with no challenge and no signature:
+ the caller MUST own the identity AND every named agent (key
+ possession never authorizes a link). The whole batch seals as
+ ONE `IDENTITY_LINKED` event on the IDENTITY stream carrying
+ the agent ids; an agent's own stream and audit history are
+ never written by identity operations. Already-linked agents
+ are skipped idempotently; a call that links nothing new seals
+ nothing.
+
+ Liveness gate: links attach only while the identity is
+ VERIFIED, and every named agent must be live — ACTIVE or
+ DEPRECATED; a terminal or pre-activation agent fails the
+ whole batch with 422 `AGENT_NOT_LINKABLE` (all-or-nothing,
+ matching the one-event batch semantics). The route is
+ per-owner rate limited (429-equivalent `RATE_LIMITED`).
+
+ Seal-before-success: success is reported only after the TL
+ acknowledges the `IDENTITY_LINKED` seal; link rows commit
+ with the acknowledgment. TL unavailable → 503
+ `TL_UNAVAILABLE`, retryable, nothing linked.
+ operationId: linkIdentityAgents
+ parameters:
+ - $ref: '#/components/parameters/IdentityIdPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityLinkRequest'
+ responses:
+ '200':
+ description: Batch linked (count of newly-created links)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityLinkResponse'
+ '403':
+ description: Caller does not own this identity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Identity, or a named agent, not found / not the caller's
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: Identity is not VERIFIED (IDENTITY_NOT_VERIFIED)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '422':
+ description: Invalid link request (empty or oversized batch — INVALID_LINK_REQUEST), or a named agent is not live (AGENT_NOT_LINKABLE — links require ACTIVE or DEPRECATED; rejected all-or-nothing)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ '503':
+ description: Transparency Log unavailable (TL_UNAVAILABLE — retryable, nothing linked)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /ans/identities/{identityId}/links/{agentId}:
+ delete:
+ tags:
+ - Verified Identities
+ summary: Unlink an agent
+ description: |
+ Ends one association and seals `IDENTITY_UNLINKED` on the
+ identity stream. The association's history persists in the
+ identity's audit chain and the raw log tiles; unlinked pairs
+ may be re-linked later. Shares the link route's per-owner
+ rate limit.
+
+ Seal-before-success: the link row flips only after the TL
+ acknowledges the seal. TL unavailable → 503 `TL_UNAVAILABLE`,
+ retryable, the link stands.
+ operationId: unlinkIdentityAgent
+ parameters:
+ - $ref: '#/components/parameters/IdentityIdPath'
+ - $ref: '#/components/parameters/AgentIdPath'
+ responses:
+ '204':
+ description: Link removed, event sealed
+ '403':
+ description: Caller does not own this identity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Identity or live link not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '503':
+ description: Transparency Log unavailable (TL_UNAVAILABLE — retryable, nothing changed)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+
+components:
+ # ──────────────────────────────────────────────────────────────────
+ # Security schemes — both providers (static API key + OIDC) accept
+ # the same `Authorization: Bearer ` shape on the wire, so
+ # one HTTP-bearer definition covers both. Swagger UI renders an
+ # "Authorize" button from this definition; paste your API key
+ # (quickstart default: `ans-dev-key-change-me`) as the bearer value
+ # and "Try it out" will include it on subsequent requests.
+ # ──────────────────────────────────────────────────────────────────
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ description: |
+ Static API key or OIDC bearer token. The RA treats the static
+ key as admin; OIDC tokens inherit admin-ness from configured
+ admin-groups (see `auth.oidc.admin-groups` in the config
+ reference). Public-read routes (TL verifier GETs) and
+ `/v2/admin/{health,ready}` and `/docs` ignore this header.
+
+ # ──────────────────────────────────────────────────────────────────
+ # Reusable parameters
+ # ──────────────────────────────────────────────────────────────────
+ parameters:
+ AgentIdPath:
+ name: agentId
+ in: path
+ description: Unique identifier of the agent (UUID)
+ required: true
+ schema:
+ type: string
+ format: uuid
+
+ IdentityIdPath:
+ name: identityId
+ in: path
+ description: Unique identifier of the verified identity (UUIDv7)
+ required: true
+ schema:
+ type: string
+
+ # ──────────────────────────────────────────────────────────────────
+ # Schemas
+ # ──────────────────────────────────────────────────────────────────
+ schemas:
+ # ── New: Cursor-paginated list response ──────────────────────
+ AgentListResponse:
+ type: object
+ description: |
+ Ownership-scoped, cursor-paginated list of the caller's agents.
+ Replaces v1's `AgentSearchResponse`. Collection array is named
+ `items` per standard REST collection-response conventions.
+ properties:
+ items:
+ type: array
+ description: List of agents owned by the caller
+ items:
+ type: object
+ properties:
+ ansName:
+ type: string
+ example: ans://v1.0.0.myagent.example.com
+ agentId:
+ type: string
+ format: uuid
+ agentDisplayName:
+ type: string
+ maxLength: 64
+ agentDescription:
+ type: string
+ version:
+ type: string
+ agentHost:
+ type: string
+ maxLength: 253
+ example: myagent.example.com
+ status:
+ $ref: '#/components/schemas/AgentLifecycleStatus'
+ ttl:
+ type: integer
+ default: 300
+ registrationTimestamp:
+ type: string
+ format: date-time
+ endpoints:
+ type: array
+ items:
+ $ref: '#/components/schemas/AgentEndpoint'
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/Link'
+ required:
+ - ansName
+ - agentId
+ - agentDisplayName
+ - version
+ - agentHost
+ - status
+ - endpoints
+ - links
+ returnedCount:
+ type: integer
+ description: Number of agents in this response
+ limit:
+ type: integer
+ description: Limit that was applied
+ nextCursor:
+ type: string
+ nullable: true
+ description: |
+ Opaque cursor for the next page. Pass as the `cursor` query
+ parameter to retrieve the next page. Null when there are no
+ more results.
+ hasMore:
+ type: boolean
+ description: Whether more results exist beyond this page
+ required:
+ - items
+ - returnedCount
+ - limit
+ - hasMore
+
+ # ── Unchanged schemas carried forward from v1 ────────────────
+ Link:
+ type: object
+ properties:
+ rel:
+ type: string
+ example: agent-details
+ href:
+ type: string
+ format: uri
+ required:
+ - rel
+ - href
+
+ Protocol:
+ type: string
+ enum: [A2A, MCP, HTTP_API]
+
+ DiscoveryProfile:
+ type: string
+ enum: [ANS_DNSAID, ANS_TXT]
+ description: |
+ Names one DNS record family the RA can emit for an agent
+ registration. Used as the element type of discoveryProfiles[].
+ Each value provisions a complete record set, not a single
+ record — the bullets below enumerate exactly what an operator
+ must publish for that profile.
+
+ - ANS_DNSAID: the DNS-AID-aligned Consolidated Approach (RFC
+ 9460), and the server default when discoveryProfiles is
+ omitted. Provisions:
+ 1. One SVCB row per protocol endpoint at the bare FQDN
+ (ServiceMode `1 .`), carrying `alpn` (the protocol
+ token: a2a / mcp / x-http), `port` (the endpoint's TLS
+ port), `key65402` (bap, the agent protocol, always
+ present), and — when the endpoint supplies them —
+ `key65400` (cap, the metadataUrl capability locator),
+ `key65401` (cap-sha256, the base64url capability digest,
+ present only when the endpoint declared a metaDataHash),
+ and `key65409` (the well-known path suffix, emitted only
+ when metadataUrl sits at
+ https://{fqdn}/.well-known/).
+ key65400/key65401/key65402/key65409 are the RFC 9460
+ §14.3.1 Private Use presentation of the DNS-AID draft-02
+ cap / cap-sha256 / bap / well-known SvcParams, which have
+ no IANA code point yet; the named forms are unpublishable
+ (strict RFC 9460 parsers reject them), so the keyNNNNN
+ forms are what reaches DNS. They switch back to named
+ forms if/when IANA registers the keys.
+ 2. One `_ans-badge` TXT at `_ans-badge.{fqdn}` (the
+ transparency-log discovery hint).
+ 3. One TLSA per distinct TLS port at `_._tcp.{fqdn}`
+ binding the server certificate (DANE-EE, full cert,
+ SHA-256), emitted only when a server certificate exists.
+ - ANS_TXT: original `_ans` TXT shape (one row per protocol at
+ `_ans.{fqdn}`), supported indefinitely for operators with
+ existing zone-edit tooling that targets `_ans.{fqdn}`;
+ opt-in via an explicit discoveryProfiles value. Emits an
+ HTTPS RR at the bare FQDN alongside carrying only `alpn=h2`
+ (an IANA-registered SvcParam — no keyNNNNN form is needed
+ here; the asymmetry with ANS_DNSAID is intentional), since
+ `_ans` TXT carries no connection hints. The HTTPS RR is
+ best-effort: operators on CNAME-fronted apexes cannot publish
+ a record at that name (RFC 1034 §3.6.2) and verification does
+ not require it. Provisions the same `_ans-badge` TXT and
+ per-port TLSA records as ANS_DNSAID.
+
+ RevocationReason:
+ type: string
+ enum:
+ - KEY_COMPROMISE
+ - CESSATION_OF_OPERATION
+ - AFFILIATION_CHANGED
+ - SUPERSEDED
+ - CERTIFICATE_HOLD
+ - PRIVILEGE_WITHDRAWN
+ - AA_COMPROMISE
+
+ AgentRegistrationRequest:
+ type: object
+ properties:
+ agentDisplayName:
+ type: string
+ maxLength: 64
+ example: sentiment-analyzer
+ agentDescription:
+ type: string
+ maxLength: 150
+ version:
+ type: string
+ pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$'
+ example: 1.2.0
+ agentHost:
+ type: string
+ maxLength: 253
+ example: myagent.example.com
+ endpoints:
+ type: array
+ minItems: 1
+ description: >-
+ One or more protocol endpoints. Each protocol may appear at
+ most once across the array — a second endpoint with an
+ already-used protocol is rejected with 422 DUPLICATE_PROTOCOL,
+ and an exact protocol+agentUrl repeat with 422
+ DUPLICATE_ENDPOINT. (By-field uniqueness is not expressible in
+ OpenAPI/JSON Schema; enforced in
+ internal/domain/endpoint.go AgentEndpoints.Validate.)
+ items:
+ $ref: '#/components/schemas/AgentEndpoint'
+ serverCsrPEM:
+ type: string
+ serverCertificatePEM:
+ type: string
+ serverCertificateChainPEM:
+ type: string
+ identityCsrPEM:
+ type: string
+ description: >-
+ Optional. When supplied, the RA issues an identity certificate
+ from this CSR at verify-acme. When omitted, the agent registers
+ without an identity certificate and cannot add one later — it
+ must register a new version instead.
+ discoveryProfiles:
+ type: array
+ items:
+ $ref: '#/components/schemas/DiscoveryProfile'
+ uniqueItems: true
+ minItems: 1
+ default: ["ANS_DNSAID"]
+ description: |
+ Set of DNS record families the RA tells the operator to
+ publish and emits in the AGENT_REGISTERED TL event's
+ attestations.dnsRecordsProvisioned[]. The computed records
+ surface to the client on GET /v2/ans/agents/{agentId} as
+ registrationPending.dnsRecords[] once the agent reaches
+ PENDING_DNS — not on the 202 register response, which
+ returns only the ACME challenge (production records are
+ deferred until verify-acme proves domain control and issues
+ the certificates the TLSA binding depends on).
+
+ Each value names one record family; an operator publishing
+ the union (DNS-AID-aligned SVCB plus the original `_ans` TXT
+ shape) sends both. Order is not significant.
+
+ Optional. Omitted (or an explicit empty array) normalizes to
+ the default ["ANS_DNSAID"] server-side; opt into the legacy
+ ["ANS_TXT"] shape explicitly. The `minItems`/`uniqueItems`
+ schema constraints are the canonical client contract for a
+ present array — validate before sending. A non-conformant
+ request (an explicit empty array, or duplicate values) is
+ handled defensively rather than rejected: empty is treated
+ as omitted and duplicates are ignored, but conformant
+ clients never send either.
+ example: ["ANS_DNSAID"]
+ required:
+ - agentDisplayName
+ - agentHost
+ - endpoints
+ - version
+ oneOf:
+ - required: [serverCsrPEM]
+ - required: [serverCertificatePEM]
+
+ AgentRevocationRequest:
+ type: object
+ properties:
+ reason:
+ $ref: '#/components/schemas/RevocationReason'
+ comments:
+ type: string
+ maxLength: 200
+ required:
+ - reason
+
+ AgentRevocationResponse:
+ type: object
+ properties:
+ agentId:
+ type: string
+ format: uuid
+ ansName:
+ type: string
+ status:
+ $ref: '#/components/schemas/AgentLifecycleStatus'
+ revokedAt:
+ type: string
+ format: date-time
+ reason:
+ $ref: '#/components/schemas/RevocationReason'
+ dnsRecordsToRemove:
+ type: array
+ items:
+ $ref: '#/components/schemas/DnsRecord'
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/Link'
+ required:
+ - agentId
+ - ansName
+ - status
+ - revokedAt
+ - reason
+ - links
+
+ AgentDetails:
+ type: object
+ properties:
+ agentId:
+ type: string
+ format: uuid
+ agentDisplayName:
+ type: string
+ maxLength: 64
+ agentDescription:
+ type: string
+ version:
+ type: string
+ agentHost:
+ type: string
+ maxLength: 253
+ endpoints:
+ type: array
+ items:
+ $ref: '#/components/schemas/AgentEndpoint'
+ ansName:
+ type: string
+ example: ans://v1.0.0.myagent.example.com
+ agentStatus:
+ $ref: '#/components/schemas/AgentLifecycleStatus'
+ registrationTimestamp:
+ type: string
+ format: date-time
+ lastRenewalTimestamp:
+ type: string
+ format: date-time
+ registrationPending:
+ $ref: '#/components/schemas/RegistrationPending'
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/Link'
+ identities:
+ type: array
+ description: |
+ Additive, optional, COMPUTED — the verified identities
+ currently linked to this agent, joined from the link rows
+ at read time. Never stored on the registration; identity
+ rotation/revocation is visible here immediately with zero
+ agent-side writes.
+ items:
+ $ref: '#/components/schemas/LinkedIdentity'
+ required:
+ - agentId
+ - ansName
+ - agentDisplayName
+ - version
+ - endpoints
+ - agentHost
+ - agentStatus
+ - links
+
+ AgentStatus:
+ type: object
+ properties:
+ status:
+ $ref: '#/components/schemas/AgentLifecycleStatus'
+ phase:
+ type: string
+ enum: [INITIALIZATION, DOMAIN_VALIDATION, CERTIFICATE_ISSUANCE, DNS_PROVISIONING, COMPLETED]
+ completedSteps:
+ type: array
+ items:
+ type: string
+ pendingSteps:
+ type: array
+ items:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ expiresAt:
+ type: string
+ format: date-time
+
+ AgentLifecycleStatus:
+ type: string
+ enum: [PENDING_VALIDATION, PENDING_DNS, ACTIVE, FAILED, EXPIRED, DEPRECATED, REVOKED]
+
+ AgentLifecycleStatusFilter:
+ type: string
+ enum: [PENDING_DNS, ACTIVE, DEPRECATED, REVOKED, ALL]
+
+ RegistrationPending:
+ type: object
+ properties:
+ agentId:
+ type: string
+ format: uuid
+ description: |
+ Unique identifier assigned to this agent registration. Required for
+ all subsequent API calls (verify-acme, verify-dns, certificates,
+ revoke). New in v2 — previously callers had to parse this from
+ HATEOAS link hrefs. Recommended for backport to v1.
+ status:
+ type: string
+ enum: [PENDING_VALIDATION, PENDING_CERTS, PENDING_DNS]
+ ansName:
+ type: string
+ example: ans://v1.0.0.external-domain.com
+ nextSteps:
+ type: array
+ items:
+ $ref: '#/components/schemas/NextStep'
+ challenges:
+ type: array
+ items:
+ $ref: '#/components/schemas/ChallengeInfo'
+ dnsRecords:
+ type: array
+ items:
+ $ref: '#/components/schemas/DnsRecord'
+ expiresAt:
+ type: string
+ format: date-time
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/Link'
+ required:
+ - agentId
+ - status
+ - ansName
+ - nextSteps
+
+ # ── AI Catalog (producer-generated discovery artifact) ──────────
+ CatalogEntry:
+ type: object
+ description: |
+ One artifact's AI Catalog record, derived from a registration (AI
+ Catalog draft 2026-06-11 §4.4). A bare entry is served as
+ application/json — it is not itself a catalog document and carries no
+ `specVersion`. Exactly one of `url` or `data` is present: a
+ single-protocol entry carries `url`; a multi-protocol agent carries
+ `data` (an inline nested catalog of per-protocol children) with
+ `mediaType` `application/ai-catalog+json`.
+ properties:
+ identifier:
+ type: string
+ description: |
+ Stable, version-spanning lineage handle
+ `urn:air:{agentHost}:agents:{label}`, where `{label}` is the
+ agent's display name with whitespace runs collapsed to single
+ hyphens — the same derivation the ARD discovery service (the
+ Finder) mints from feed events, so search results and the
+ published catalog carry one identifier per agent. Never the
+ per-version agentId.
+ example: urn:air:ai-agent.acmecorp.com:agents:Support-Assistant
+ displayName:
+ type: string
+ maxLength: 64
+ description:
+ type: string
+ maxLength: 150
+ version:
+ type: string
+ example: 2.1.0
+ mediaType:
+ type: string
+ description: |
+ Artifact discriminator: `application/a2a-agent-card+json`,
+ `application/mcp-server-card+json`, or
+ `application/ai-catalog+json` for a multi-protocol nested entry.
+ example: application/a2a-agent-card+json
+ url:
+ type: string
+ format: uri
+ description: |
+ The endpoint's metaDataUrl (the protocol card location). Present
+ on a single-protocol or nested-child entry. Mutually exclusive
+ with `data`.
+ data:
+ allOf:
+ - $ref: '#/components/schemas/CatalogNested'
+ description: |
+ Inline nested catalog of per-protocol children, present only on a
+ multi-protocol outer entry. Mutually exclusive with `url`.
+ tags:
+ type: array
+ items:
+ type: string
+ updatedAt:
+ type: string
+ format: date-time
+ publisher:
+ $ref: '#/components/schemas/CatalogPublisher'
+ metadata:
+ $ref: '#/components/schemas/CatalogEntryMetadata'
+ trustManifest:
+ $ref: '#/components/schemas/CatalogTrustManifest'
+ required:
+ - identifier
+ - displayName
+ - mediaType
+
+ CatalogNested:
+ type: object
+ description: |
+ Inline nested catalog held in a multi-protocol entry's `data` (AI
+ Catalog §6.1 / IMPL §3.5).
+ properties:
+ specVersion:
+ type: string
+ example: "1.0"
+ entries:
+ type: array
+ items:
+ $ref: '#/components/schemas/CatalogEntry'
+ required:
+ - specVersion
+ - entries
+
+ CatalogDocument:
+ type: object
+ description: |
+ A host-complete AI Catalog document (AI Catalog §4.2), served as
+ application/ai-catalog+json. The file an AHP publishes at
+ `/.well-known/ai-catalog.json`; `entries` carries one CatalogEntry
+ per ACTIVE, catalog-eligible agent on the host (sorted by
+ identifier then version for a stable ETag).
+ properties:
+ specVersion:
+ type: string
+ example: "1.0"
+ host:
+ $ref: '#/components/schemas/CatalogHostInfo'
+ entries:
+ type: array
+ items:
+ $ref: '#/components/schemas/CatalogEntry'
+ required:
+ - specVersion
+ - entries
+
+ CatalogHostInfo:
+ type: object
+ description: |
+ Host Info object (AI Catalog §4.3) identifying the operator of a
+ catalog document. `displayName` is required within Host Info; for a
+ per-host document both fields are the agentHost.
+ properties:
+ identifier:
+ type: string
+ example: ai-agent.acmecorp.com
+ displayName:
+ type: string
+ example: ai-agent.acmecorp.com
+ required:
+ - displayName
+
+ CatalogPublisher:
+ type: object
+ description: |
+ Publishing entity (AI Catalog §4.6). DNS-anchored in this RA;
+ `identifier` and `displayName` are both the agentHost.
+ properties:
+ identifier:
+ type: string
+ example: ai-agent.acmecorp.com
+ displayName:
+ type: string
+ example: ai-agent.acmecorp.com
+ identityType:
+ type: string
+ example: dns
+ required:
+ - identifier
+ - displayName
+
+ CatalogEntryMetadata:
+ type: object
+ description: |
+ Entry-level identifiers a consumer verifies against (IMPL §5.3). No
+ `logId` — the TL is keyed by agentId, not logId.
+ properties:
+ ansName:
+ type: string
+ example: ans://v2.1.0.ai-agent.acmecorp.com
+ agentHost:
+ type: string
+ example: ai-agent.acmecorp.com
+ badgeUrl:
+ type: string
+ format: uri
+ description: |
+ TL status / card-integrity surface (`/v1/agents/{agentId}`).
+ Omitted when no TL base URL is configured.
+ required:
+ - ansName
+ - agentHost
+
+ CatalogTrustManifest:
+ type: object
+ description: |
+ Verifiable identity + trust metadata (AI Catalog §5). `identity`
+ MUST equal the containing entry's `identifier`.
+ properties:
+ identity:
+ type: string
+ example: urn:air:ai-agent.acmecorp.com:agents:ai-agent
+ attestations:
+ type: array
+ description: |
+ Present only when a TL base URL is configured. The slice-1
+ attestation is the ANS-Registration SCITT receipt.
+ items:
+ $ref: '#/components/schemas/CatalogAttestation'
+ required:
+ - identity
+
+ CatalogAttestation:
+ type: object
+ description: |
+ A verifiable claim (AI Catalog §5.4). The ANS-Registration
+ attestation points at the agent's SCITT receipt on the Transparency
+ Log; it carries no digest (the receipt proves inclusion, not entry
+ content).
+ properties:
+ type:
+ type: string
+ example: ANS-Registration
+ uri:
+ type: string
+ format: uri
+ example: https://tl.example.org/v1/agents/550e8400-e29b-41d4-a716-446655440000/receipt
+ mediaType:
+ type: string
+ example: application/scitt-receipt+cose
+ required:
+ - type
+ - uri
+ - mediaType
+
+ NextStep:
+ type: object
+ properties:
+ action:
+ type: string
+ enum: [CONFIGURE_DNS, CONFIGURE_HTTP, VERIFY_DNS, VALIDATE_DOMAIN, WAIT, CANCEL]
+ description:
+ type: string
+ endpoint:
+ type: string
+ format: uri
+ estimatedTimeMinutes:
+ type: integer
+
+ ChallengeInfo:
+ type: object
+ properties:
+ type:
+ type: string
+ enum: [DNS_01, HTTP_01]
+ token:
+ type: string
+ keyAuthorization:
+ type: string
+ dnsRecord:
+ type: object
+ properties:
+ name:
+ type: string
+ type:
+ type: string
+ value:
+ type: string
+ httpPath:
+ type: string
+ expiresAt:
+ type: string
+ format: date-time
+
+ DnsRecord:
+ type: object
+ properties:
+ name:
+ type: string
+ type:
+ type: string
+ enum: [HTTPS, SVCB, TLSA, TXT]
+ value:
+ type: string
+ priority:
+ type: integer
+ ttl:
+ type: integer
+ default: 3600
+ purpose:
+ type: string
+ enum: [DISCOVERY, TRUST, CERTIFICATE_BINDING, BADGE]
+ required:
+ type: boolean
+ default: true
+ required:
+ - name
+ - type
+ - value
+
+ AgentEndpoint:
+ type: object
+ properties:
+ agentUrl:
+ type: string
+ format: uri
+ example: https://myagent.example.com/mcp
+ metaDataUrl:
+ type: string
+ format: uri
+ maxLength: 2048
+ description: >-
+ Optional. The endpoint's metadata descriptor URL. Must be
+ https and free of whitespace, quotes, and other characters
+ that require SVCB presentation escaping — it is emitted
+ verbatim as the ANS_DNSAID `cap` SvcParam (key65400) and is
+ the document `cap-sha256` (key65401) digests. When it sits at
+ https://{agentHost}/.well-known/, the suffix is also
+ advertised as the `well-known` SvcParam (key65409). NOTE: a
+ `cap` whose host differs from the agent FQDN is published
+ without an integrity pin unless metaDataHash is also supplied
+ — consumers SHOULD prefer a metaDataHash-pinned descriptor for
+ off-host metadata.
+ metaDataHash:
+ type: string
+ pattern: '^SHA256:[a-f0-9]{64}$'
+ description: >-
+ Optional integrity pin over the metadata descriptor.
+ Meaningless on its own: it MUST be accompanied by metaDataUrl.
+ A metaDataHash without a metaDataUrl is rejected with 422 INVALID_ENDPOINT.
+ documentationUrl:
+ type: string
+ format: uri
+ protocol:
+ $ref: '#/components/schemas/Protocol'
+ functions:
+ type: array
+ items:
+ $ref: '#/components/schemas/AgentFunction'
+ transports:
+ type: array
+ items:
+ type: string
+ enum: [STREAMABLE_HTTP, SSE, JSON_RPC, GRPC, REST, HTTP]
+ required:
+ - protocol
+ - agentUrl
+ anyOf:
+ - not:
+ required: [metaDataHash]
+ - required: [metaDataUrl]
+
+ AgentFunction:
+ type: object
+ properties:
+ id:
+ type: string
+ maxLength: 64
+ name:
+ type: string
+ maxLength: 64
+ tags:
+ type: array
+ maxItems: 5
+ items:
+ type: string
+ maxLength: 20
+ required:
+ - id
+ - name
+
+ # ── Events feed (GET /v1/agents/events) ──────────────────────
+ # Byte-compatible with the production `getAgentEvents` response.
+ # Reuses AgentEndpoint / AgentFunction above (the feed's endpoint
+ # shape is the same minus metaDataHash, which the feed never emits).
+ EventPageResponse:
+ type: object
+ description: Paginated response containing ANS events.
+ properties:
+ items:
+ type: array
+ description: Array of event items (always present; `[]` when empty).
+ items:
+ $ref: '#/components/schemas/EventItem'
+ lastLogId:
+ type: string
+ description: |
+ The logId of the last event in this page. Pass it as the next
+ request's `lastLogId` to fetch the following page. Omitted
+ when there are no more results.
+ required:
+ - items
+
+ EventItem:
+ type: object
+ description: One ANS lifecycle event with agent details and event metadata.
+ properties:
+ logId:
+ type: string
+ description: Unique identifier for this event in the stream.
+ example: 019a7a52-e5bf-7b5b-b048-d0b78f4b4c5f
+ eventType:
+ type: string
+ description: Type of ANS event.
+ enum: [AGENT_DEPRECATED, AGENT_REGISTERED, AGENT_REVOKED, AGENT_RENEWED]
+ example: AGENT_REGISTERED
+ createdAt:
+ type: string
+ format: date-time
+ description: Timestamp when the event was created (producer time).
+ example: '2025-01-08T12:30:00Z'
+ expiresAt:
+ type: string
+ format: date-time
+ description: When the agent's registration expires (if applicable).
+ example: '2026-01-08T12:30:00Z'
+ agentId:
+ type: string
+ format: uuid
+ description: Unique identifier of the agent.
+ example: 550e8400-e29b-41d4-a716-446655440000
+ ansName:
+ type: string
+ description: Fully qualified ANS name (ans://{version}.{agentHost}).
+ example: ans://v1.0.0.myagent.example.com
+ agentHost:
+ type: string
+ maxLength: 253
+ description: The agent's hosting domain.
+ example: myagent.example.com
+ agentDisplayName:
+ type: string
+ maxLength: 64
+ description: Human-readable display name for the agent.
+ example: Sentiment Analyzer
+ agentDescription:
+ type: string
+ maxLength: 150
+ description: Description of the agent.
+ example: An agent that analyzes sentiment in text
+ version:
+ type: string
+ description: Semantic version of the agent.
+ example: 1.0.0
+ providerId:
+ type: string
+ description: |
+ Provider identifier (production field). This OSS RA never
+ emits it — the only principal id is the registrant's owner
+ id, which is not exposed.
+ example: PC_1234567890
+ endpoints:
+ type: array
+ description: Agent endpoints with protocol-specific configuration.
+ items:
+ $ref: '#/components/schemas/AgentEndpoint'
+ required:
+ - logId
+ - eventType
+ - createdAt
+ - agentId
+ - ansName
+ - agentHost
+ - version
+
+ CertificateResponse:
+ type: object
+ properties:
+ csrId:
+ type: string
+ format: uuid
+ certificateSubject:
+ type: string
+ nullable: true
+ certificateIssuer:
+ type: string
+ nullable: true
+ certificateSerialNumber:
+ type: string
+ nullable: true
+ certificateValidFrom:
+ type: string
+ format: date-time
+ certificateValidTo:
+ type: string
+ format: date-time
+ certificatePEM:
+ type: string
+ chainPEM:
+ type: string
+ nullable: true
+ certificatePublicKeyAlgorithm:
+ type: string
+ nullable: true
+ certificateSignatureAlgorithm:
+ type: string
+ nullable: true
+ required:
+ - csrId
+ - certificatePEM
+ - certificateValidFrom
+ - certificateValidTo
+
+ CsrSubmissionRequest:
+ type: object
+ properties:
+ csrPEM:
+ type: string
+ required:
+ - csrPEM
+
+ CsrSubmissionResponse:
+ type: object
+ properties:
+ csrId:
+ type: string
+ format: uuid
+ message:
+ type: string
+ required:
+ - csrId
+
+ CsrStatusResponse:
+ type: object
+ properties:
+ csrId:
+ type: string
+ format: uuid
+ type:
+ type: string
+ enum: [SERVER, IDENTITY]
+ status:
+ type: string
+ enum: [PENDING, SIGNED, REJECTED]
+ submittedAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ failureReason:
+ type: string
+ nullable: true
+ required:
+ - csrId
+ - type
+ - status
+ - submittedAt
+ - updatedAt
+
+ ServerCertificateRenewalRequest:
+ type: object
+ properties:
+ serverCsrPEM:
+ type: string
+ serverCertificatePEM:
+ type: string
+ serverCertificateChainPEM:
+ type: string
+ oneOf:
+ - required: [serverCsrPEM]
+ - required: [serverCertificatePEM]
+
+ RenewalSubmissionResponse:
+ type: object
+ properties:
+ renewalType:
+ type: string
+ enum: [SERVER_CSR, SERVER_BYOC]
+ status:
+ type: string
+ enum: [PENDING_VALIDATION, ISSUING_CERTIFICATE]
+ csrId:
+ type: string
+ format: uuid
+ nullable: true
+ challenges:
+ type: object
+ nullable: true
+ properties:
+ dns01:
+ $ref: '#/components/schemas/ChallengeInfo'
+ http01:
+ $ref: '#/components/schemas/ChallengeInfo'
+ expiresAt:
+ type: string
+ format: date-time
+ nextStep:
+ $ref: '#/components/schemas/NextStep'
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/Link'
+ required:
+ - renewalType
+ - status
+ - expiresAt
+ - nextStep
+
+ RenewalStatusResponse:
+ type: object
+ properties:
+ renewalType:
+ type: string
+ enum: [SERVER_CSR, SERVER_BYOC]
+ status:
+ type: string
+ enum: [PENDING_VALIDATION, ISSUING_CERTIFICATE, COMPLETED, FAILED, EXPIRED]
+ csrId:
+ type: string
+ format: uuid
+ nullable: true
+ challenges:
+ type: object
+ nullable: true
+ properties:
+ dns01:
+ $ref: '#/components/schemas/ChallengeInfo'
+ http01:
+ $ref: '#/components/schemas/ChallengeInfo'
+ tlsaDnsRecord:
+ $ref: '#/components/schemas/DnsRecord'
+ nullable: true
+ failureReason:
+ type: string
+ nullable: true
+ expiresAt:
+ type: string
+ format: date-time
+ nextStep:
+ $ref: '#/components/schemas/NextStep'
+ required:
+ - renewalType
+ - status
+ - expiresAt
+ - nextStep
+
+ RenewalVerificationResponse:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [VERIFIED, ISSUING_CERTIFICATE, COMPLETED]
+ csrId:
+ type: string
+ format: uuid
+ nullable: true
+ tlsaDnsRecord:
+ $ref: '#/components/schemas/DnsRecord'
+ nullable: true
+ nextStep:
+ $ref: '#/components/schemas/NextStep'
+ required:
+ - status
+ - nextStep
+
+ Problem:
+ type: object
+ description: |
+ RFC 7807 problem details — the body every RA error response
+ carries, served as `application/problem+json`. `type`, `title`,
+ and `status` are always present; `detail` is the human-readable
+ explanation specific to this occurrence, and `code` is a stable,
+ machine-readable error identifier (read `code` for programmatic
+ handling, `detail` for display).
+ properties:
+ type:
+ type: string
+ description: A URI reference identifying the problem type.
+ example: about:blank
+ title:
+ type: string
+ description: Short, human-readable summary of the problem type.
+ example: Validation Failed
+ status:
+ type: integer
+ description: The HTTP status code for this occurrence.
+ example: 422
+ detail:
+ type: string
+ description: Human-readable explanation specific to this occurrence.
+ code:
+ type: string
+ description: Stable, machine-readable error code.
+ example: INVALID_LIMIT
+ required:
+ - type
+ - title
+ - status
+
+ Problem:
+ type: object
+ description: |
+ RFC 7807 Problem Details — the actual error shape every RA failure
+ path emits, served as `application/problem+json`. Clients read
+ `code` for programmatic handling and `detail` for the
+ human-readable explanation.
+
+ (The legacy `ErrorResponse` schema below is retained only for the
+ pre-existing routes still declared against it; new routes point
+ here. Migrating the remaining declarations is tracked separately.)
+ properties:
+ type:
+ type: string
+ description: URI reference identifying the problem type.
+ title:
+ type: string
+ description: Short, human-readable summary of the problem type.
+ status:
+ type: integer
+ description: HTTP status code.
+ detail:
+ type: string
+ description: Human-readable explanation specific to this occurrence.
+ code:
+ type: string
+ description: Stable, machine-readable error code for programmatic handling.
+ required:
+ - title
+ - status
+ - code
+
+ ErrorResponse:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [ERROR]
+ code:
+ type: string
+ message:
+ type: string
+ details:
+ type: object
+ additionalProperties: true
+ required:
+ - status
+ - code
+ - message
+
+ DnsVerificationError:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [ERROR]
+ missingRecords:
+ type: array
+ items:
+ $ref: '#/components/schemas/DnsRecord'
+ incorrectRecords:
+ type: array
+ items:
+ type: object
+ properties:
+ record:
+ $ref: '#/components/schemas/DnsRecord'
+ found:
+ type: string
+ description: |
+ The live record value observed when a record exists
+ but does not match the required value.
+ expected:
+ type: string
+ description: |
+ The required value; equals record.value.
+
+ # ────────────────────────────────────────────────────────────────
+ # Verified Identities
+ # ────────────────────────────────────────────────────────────────
+ IdentityRegistrationRequest:
+ type: object
+ description: |
+ Registers (POST) or rotates (PUT) an identifier. The kind is
+ inferred from the value's lexical form — `did:web:` prefix,
+ `did:key:` prefix, or a 20-character LEI — never
+ caller-asserted.
+ properties:
+ value:
+ type: string
+ description: The identifier to prove control of
+ example: did:web:identity.acme-corp.com
+ vleiPresentation:
+ $ref: '#/components/schemas/VLEIPresentation'
+ required:
+ - value
+
+ VLEIPresentation:
+ type: object
+ description: |
+ The lei (vLEI) register-time credential presentation. REQUIRED
+ for the `lei` kind and omitted for the JWS kinds (`did:web`,
+ `did:key`). The RA submits the CESR to its configured
+ vlei-verifier, which derives and pins the subject AID; the
+ 202's `presentationStatus` reports the verifier's advisory
+ authorization decision.
+ properties:
+ cesr:
+ type: string
+ description: |
+ The full-chain CESR export of the vLEI credential and its
+ supporting KELs/ACDCs (the `credentials().get(said, true)`
+ shape). The RA never parses KERI key state itself — the
+ verifier is the authoritative key-state oracle.
+ required:
+ - cesr
+
+ IdentityChallengeResponse:
+ type: object
+ description: |
+ The 202 challenge round. Every entry shares the same
+ anti-replay nonce and the same signingInput — the input is
+ key-independent; entries enumerate the keys the resolver
+ could see in advance (a single unkeyed entry when it could
+ not — name keys via the JWS `kid` header at verify time).
+ properties:
+ identityId:
+ type: string
+ description: RA-assigned UUIDv7 — the TL stream key
+ kind:
+ type: string
+ enum: ['did:web', 'did:key', 'lei']
+ value:
+ type: string
+ description: The canonical identifier this round proves
+ status:
+ $ref: '#/components/schemas/IdentityLifecycleStatus'
+ nonce:
+ type: string
+ description: Base64url 32-byte single-use anti-replay nonce
+ expiresAt:
+ type: string
+ format: date-time
+ challenges:
+ type: array
+ items:
+ $ref: '#/components/schemas/IdentityProofChallenge'
+ presentationStatus:
+ type: string
+ enum: [AUTHORIZED, PENDING]
+ description: |
+ The lei register-time advisory authorization status from
+ the vlei-verifier (`AUTHORIZED` | `PENDING`). Omitted for
+ kinds with no register-time presentation (`did:web`,
+ `did:key`). Advisory only — control is finally established
+ by a LIVE re-authorization at verify-control.
+ required:
+ - identityId
+ - kind
+ - value
+ - status
+ - nonce
+ - expiresAt
+ - challenges
+
+ IdentityProofChallenge:
+ type: object
+ properties:
+ kid:
+ type: string
+ description: |
+ Verification-method id eligible to sign this round
+ (omitted when the resolver could not enumerate keys)
+ example: did:web:identity.acme-corp.com#key-1
+ signingInput:
+ type: string
+ description: |
+ Base64url of the exact RFC 8785 (JCS) canonical
+ IdentityProofInput bytes — {identifier, identityId,
+ nonce, purpose:"ans:identity-proof:v1", raId, scheme}. A
+ compact JWS's payload segment MUST equal this string
+ verbatim; clients never canonicalize.
+ required:
+ - signingInput
+
+ VerifyControlRequest:
+ type: object
+ description: |
+ The control proof. Its members are additive per identifier
+ kind, and EXACTLY ONE family is set per request:
+
+ - JWS kinds (`did:web`, `did:key`) submit `signedProofs` —
+ one compact JWS per proven key.
+ - The `lei` kind submits `cesrSignature` — a single CESR
+ signature over the served signingInput by the subject
+ AID's current key.
+
+ Supported JWS algorithms match what the verifier implements:
+ EdDSA (Ed25519), ES256 (ECDSA P-256), and RS256 (RSA >=
+ 2048). Key-agreement keys (X25519) and curves without a
+ verifier (secp256k1, P-384/521) are rejected with a precise
+ error.
+ properties:
+ signedProofs:
+ type: array
+ minItems: 1
+ maxItems: 16
+ items:
+ type: string
+ description: |
+ Compact JWS over the served signingInput. The protected
+ header carries `kid` (the claimed verification method)
+ and MAY carry `jwk` (the signer's public key — required
+ by the quickstart noop resolver, ignored by the web
+ resolver, which always uses the resolved document).
+ cesrSignature:
+ type: string
+ description: |
+ The lei proof: a single CESR signature over the served
+ signingInput by the subject AID's current key. Set only
+ for the `lei` kind. The RA forwards it to the
+ vlei-verifier, which resolves the AID's key state from its
+ KEL and checks the signature over the exact signingInput
+ bytes (the same payload the JWS kinds sign).
+ oneOf:
+ - required: [signedProofs]
+ - required: [cesrSignature]
+
+ IdentityLifecycleStatus:
+ type: string
+ enum: [PENDING_CONTROL, VERIFIED, REVOKED]
+ description: |
+ PENDING_CONTROL → VERIFIED → REVOKED. Rotation keeps the row
+ VERIFIED (the staged replacement proves control before
+ anything changes).
+
+ IdentityListResponse:
+ type: object
+ description: |
+ Ownership-scoped, cursor-paginated list of the caller's
+ verified identities. Mirrors `AgentListResponse`: the
+ collection array is named `items` per the standard REST
+ collection-response convention shared across the v2 surface.
+ properties:
+ items:
+ type: array
+ description: List of identities owned by the caller
+ items:
+ $ref: '#/components/schemas/IdentityDetails'
+ returnedCount:
+ type: integer
+ description: Number of identities in this response
+ limit:
+ type: integer
+ description: Limit that was applied
+ nextCursor:
+ type: string
+ nullable: true
+ description: |
+ Opaque cursor for the next page. Pass as the `cursor` query
+ parameter to retrieve the next page. Null when there are no
+ more results.
+ hasMore:
+ type: boolean
+ description: Whether more results exist beyond this page
+ required:
+ - items
+ - returnedCount
+ - limit
+ - hasMore
+
+ IdentityDetails:
+ type: object
+ properties:
+ identityId:
+ type: string
+ kind:
+ type: string
+ enum: ['did:web', 'did:key', 'lei']
+ value:
+ type: string
+ status:
+ $ref: '#/components/schemas/IdentityLifecycleStatus'
+ proofMethod:
+ type: string
+ description: Control proof that verified this identity
+ enum: [did-web-sig, did-key-sig, lei-vlei-acdc]
+ pendingValue:
+ type: string
+ description: Staged rotation replacement (empty unless rotating)
+ verifiedAt:
+ type: string
+ format: date-time
+ createdAt:
+ type: string
+ format: date-time
+ linkedAgents:
+ type: array
+ description: Live links (detail responses only)
+ items:
+ type: object
+ properties:
+ agentId:
+ type: string
+ format: uuid
+ linkedAt:
+ type: string
+ format: date-time
+ required:
+ - agentId
+ required:
+ - identityId
+ - kind
+ - value
+ - status
+ - createdAt
+
+ IdentityLinkRequest:
+ type: object
+ description: |
+ The batch of the caller's agents to bind — one owner-gated
+ call, no challenge, no signature; the whole batch seals as
+ ONE IDENTITY_LINKED event on the identity stream.
+ properties:
+ agentIds:
+ type: array
+ minItems: 1
+ maxItems: 256
+ items:
+ type: string
+ format: uuid
+ required:
+ - agentIds
+
+ IdentityLinkResponse:
+ type: object
+ properties:
+ linked:
+ type: integer
+ description: Newly-created links (already-linked agents are skipped)
+ required:
+ - linked
+
+ LinkedIdentity:
+ type: object
+ description: One computed identities[] entry on AgentDetails.
+ properties:
+ identityId:
+ type: string
+ kind:
+ type: string
+ enum: ['did:web', 'did:key', 'lei']
+ value:
+ type: string
+ identityStatus:
+ type: string
+ enum: [VERIFIED, REVOKED]
+ description: The identity's CURRENT status — reflects its stream now
+ linkedAt:
+ type: string
+ format: date-time
+ required:
+ - identityId
+ - kind
+ - value
+ - identityStatus
diff --git a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java
new file mode 100644
index 0000000..946c76e
--- /dev/null
+++ b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java
@@ -0,0 +1,32 @@
+package com.godaddy.ans.sdk.discovery;
+
+/**
+ * Request body for the deprecated {@code POST /v1/agents/resolution} endpoint.
+ *
+ * Not generated from the OpenAPI spec: the v2 spec no longer documents this
+ * v1-only endpoint, so this local DTO fills in for the removed
+ * {@code AgentCapabilityRequest} model.
+ */
+final class AgentCapabilityRequest {
+
+ private String agentHost;
+ private String version;
+
+ AgentCapabilityRequest agentHost(String agentHost) {
+ this.agentHost = agentHost;
+ return this;
+ }
+
+ AgentCapabilityRequest version(String version) {
+ this.version = version;
+ return this;
+ }
+
+ public String getAgentHost() {
+ return agentHost;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+}
\ No newline at end of file
diff --git a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java
index 633e38b..fb49e24 100644
--- a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java
+++ b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java
@@ -10,7 +10,6 @@
import com.godaddy.ans.sdk.exception.AnsServerException;
import com.godaddy.ans.sdk.exception.AnsValidationException;
import com.godaddy.ans.sdk.http.HttpClientFactory;
-import com.godaddy.ans.sdk.model.generated.AgentCapabilityRequest;
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import java.io.IOException;
diff --git a/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java b/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java
index 41d8733..9cffaa5 100644
--- a/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java
+++ b/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java
@@ -165,7 +165,7 @@ void shouldResolveAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
AgentDetails result = client.resolve(TEST_AGENT_HOST, "^1.0.0");
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
assertThat(result.getAgentHost()).isEqualTo(TEST_AGENT_HOST);
assertThat(result.getAgentDisplayName()).isEqualTo("Booking Agent");
assertThat(result.getAgentDescription()).isEqualTo("A booking agent for scheduling appointments");
@@ -225,7 +225,7 @@ void shouldResolveAgentWithoutVersion(WireMockRuntimeInfo wmRuntimeInfo) {
AgentDetails result = client.resolve(TEST_AGENT_HOST);
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
// Verify wildcard version was used
verify(postRequestedFor(urlEqualTo("/v1/agents/resolution"))
@@ -259,7 +259,7 @@ void shouldResolveAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception
AgentDetails result = future.get();
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
}
// ==================== Resolution Error Tests ====================
@@ -444,7 +444,7 @@ void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
AgentDetails result = client.getAgent(TEST_AGENT_ID);
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
assertThat(result.getAgentHost()).isEqualTo(TEST_AGENT_HOST);
assertThat(result.getVersion()).isEqualTo("1.0.0");
assertThat(result.getAgentStatus()).isEqualTo(AgentLifecycleStatus.ACTIVE);
@@ -517,7 +517,7 @@ void shouldGetAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
AgentDetails result = future.get();
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
}
@Test
@@ -583,7 +583,7 @@ void shouldHandleRelativeHrefInResolutionResponse(WireMockRuntimeInfo wmRuntimeI
AgentDetails result = client.resolve(TEST_AGENT_HOST);
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
}
@Test
diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java
index 6dc6afa..6ebfbf8 100644
--- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java
+++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java
@@ -9,6 +9,7 @@
import com.godaddy.ans.sdk.model.generated.AgentRevocationRequest;
import com.godaddy.ans.sdk.model.generated.AgentRevocationResponse;
import com.godaddy.ans.sdk.model.generated.AgentStatus;
+import com.godaddy.ans.sdk.model.generated.RevocationReason;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
@@ -166,7 +167,7 @@ public AgentRevocationResponse revokeAgent(String agentId, AgentRevocationReques
* @return the revocation response
* @see #revokeAgent(String, AgentRevocationRequest)
*/
- public AgentRevocationResponse revokeAgent(String agentId, AgentRevocationRequest.ReasonEnum reason) {
+ public AgentRevocationResponse revokeAgent(String agentId, RevocationReason reason) {
AgentRevocationRequest request = new AgentRevocationRequest().reason(reason);
return revokeAgent(agentId, request);
}
@@ -222,7 +223,7 @@ public CompletableFuture revokeAgentAsync(String agentI
* @return a CompletableFuture with the revocation response
*/
public CompletableFuture revokeAgentAsync(String agentId,
- AgentRevocationRequest.ReasonEnum reason) {
+ RevocationReason reason) {
return CompletableFuture.supplyAsync(() -> revokeAgent(agentId, reason), AnsExecutors.sharedIoExecutor());
}
diff --git a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java
index be20716..6bc4219 100644
--- a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java
+++ b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java
@@ -16,7 +16,9 @@
import com.godaddy.ans.sdk.model.generated.AgentRevocationRequest;
import com.godaddy.ans.sdk.model.generated.AgentRevocationResponse;
import com.godaddy.ans.sdk.model.generated.AgentStatus;
+import com.godaddy.ans.sdk.model.generated.Protocol;
import com.godaddy.ans.sdk.model.generated.RegistrationPending;
+import com.godaddy.ans.sdk.model.generated.RevocationReason;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
@@ -153,7 +155,7 @@ void shouldRegisterAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
.version("1.0.0")
.agentHost("test-agent.example.com")
.addEndpointsItem(new AgentEndpoint()
- .protocol(AgentEndpoint.ProtocolEnum.A2_A)
+ .protocol(Protocol.A2_A)
.agentUrl(URI.create("https://test-agent.example.com/a2a")))
.identityCsrPEM("-----BEGIN CERTIFICATE REQUEST-----\ntest\n-----END CERTIFICATE REQUEST-----")
.serverCsrPEM("-----BEGIN CERTIFICATE REQUEST-----\ntest\n-----END CERTIFICATE REQUEST-----");
@@ -161,7 +163,7 @@ void shouldRegisterAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
AgentDetails result = client.registerAgent(request);
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId().toString()).isEqualTo(TEST_AGENT_ID);
assertThat(result.getAnsName()).isEqualTo("ans://v1.0.0.test-agent.example.com");
assertThat(result.getRegistrationPending()).isNotNull();
assertThat(result.getRegistrationPending().getStatus())
@@ -197,7 +199,7 @@ void shouldThrowValidationExceptionOn422(WireMockRuntimeInfo wmRuntimeInfo) {
.version("invalid")
.agentHost("test-agent.example.com")
.addEndpointsItem(new AgentEndpoint()
- .protocol(AgentEndpoint.ProtocolEnum.A2_A)
+ .protocol(Protocol.A2_A)
.agentUrl(URI.create("https://test-agent.example.com/a2a")))
.identityCsrPEM("test-csr")
.serverCsrPEM("test-csr");
@@ -321,7 +323,7 @@ void shouldRevokeAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
.build();
AgentRevocationRequest request = new AgentRevocationRequest()
- .reason(AgentRevocationRequest.ReasonEnum.CESSATION_OF_OPERATION)
+ .reason(RevocationReason.CESSATION_OF_OPERATION)
.comments("Agent being decommissioned");
AgentRevocationResponse result = client.revokeAgent(TEST_AGENT_ID, request);
@@ -329,7 +331,7 @@ void shouldRevokeAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
assertThat(result).isNotNull();
assertThat(result.getAgentId()).hasToString(TEST_AGENT_ID);
assertThat(result.getStatus()).isEqualTo(AgentLifecycleStatus.REVOKED);
- assertThat(result.getReason()).isEqualTo(AgentRevocationResponse.ReasonEnum.CESSATION_OF_OPERATION);
+ assertThat(result.getReason()).isEqualTo(RevocationReason.CESSATION_OF_OPERATION);
assertThat(result.getDnsRecordsToRemove()).hasSize(3);
verify(postRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
@@ -356,7 +358,7 @@ void shouldRevokeAgentWithJustReason(WireMockRuntimeInfo wmRuntimeInfo) {
.build();
AgentRevocationResponse result = client.revokeAgent(TEST_AGENT_ID,
- AgentRevocationRequest.ReasonEnum.KEY_COMPROMISE);
+ RevocationReason.KEY_COMPROMISE);
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(AgentLifecycleStatus.REVOKED);
@@ -383,7 +385,7 @@ void shouldThrowNotFoundExceptionForRevoke(WireMockRuntimeInfo wmRuntimeInfo) {
.build();
assertThatThrownBy(() -> client.revokeAgent(TEST_AGENT_ID,
- AgentRevocationRequest.ReasonEnum.CESSATION_OF_OPERATION))
+ RevocationReason.CESSATION_OF_OPERATION))
.isInstanceOf(AnsNotFoundException.class)
.hasMessageContaining("not found");
}
@@ -407,7 +409,7 @@ void shouldThrowValidationExceptionWhenAlreadyRevoked(WireMockRuntimeInfo wmRunt
.build();
assertThatThrownBy(() -> client.revokeAgent(TEST_AGENT_ID,
- AgentRevocationRequest.ReasonEnum.CESSATION_OF_OPERATION))
+ RevocationReason.CESSATION_OF_OPERATION))
.isInstanceOf(AnsValidationException.class)
.hasMessageContaining("Validation error");
}
@@ -431,7 +433,7 @@ void shouldThrowValidationExceptionForPendingValidation(WireMockRuntimeInfo wmRu
.build();
assertThatThrownBy(() -> client.revokeAgent(TEST_AGENT_ID,
- AgentRevocationRequest.ReasonEnum.CESSATION_OF_OPERATION))
+ RevocationReason.CESSATION_OF_OPERATION))
.isInstanceOf(AnsValidationException.class)
.hasMessageContaining("Validation error");
}
@@ -458,7 +460,7 @@ void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
AgentDetails result = client.getAgent(TEST_AGENT_ID);
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId().toString()).isEqualTo(TEST_AGENT_ID);
assertThat(result.getAnsName()).isEqualTo("ans://v1.0.0.test-agent.example.com");
}
@@ -514,7 +516,7 @@ void shouldRegisterAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio
.version("1.0.0")
.agentHost("test-agent.example.com")
.addEndpointsItem(new AgentEndpoint()
- .protocol(AgentEndpoint.ProtocolEnum.A2_A)
+ .protocol(Protocol.A2_A)
.agentUrl(URI.create("https://test-agent.example.com/a2a")))
.identityCsrPEM("test-csr")
.serverCsrPEM("test-csr");
@@ -522,7 +524,7 @@ void shouldRegisterAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio
AgentDetails result = client.registerAgentAsync(request).get();
assertThat(result).isNotNull();
- assertThat(result.getAgentId()).isEqualTo(TEST_AGENT_ID);
+ assertThat(result.getAgentId().toString()).isEqualTo(TEST_AGENT_ID);
}
@Test
@@ -589,7 +591,7 @@ void shouldRevokeAgentAsyncWithRequest(WireMockRuntimeInfo wmRuntimeInfo) throws
.build();
AgentRevocationRequest request = new AgentRevocationRequest()
- .reason(AgentRevocationRequest.ReasonEnum.CESSATION_OF_OPERATION);
+ .reason(RevocationReason.CESSATION_OF_OPERATION);
AgentRevocationResponse result = client.revokeAgentAsync(TEST_AGENT_ID, request).get();
@@ -615,7 +617,7 @@ void shouldRevokeAgentAsyncWithReason(WireMockRuntimeInfo wmRuntimeInfo) throws
.build();
AgentRevocationResponse result = client.revokeAgentAsync(TEST_AGENT_ID,
- AgentRevocationRequest.ReasonEnum.KEY_COMPROMISE).get();
+ RevocationReason.KEY_COMPROMISE).get();
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(AgentLifecycleStatus.REVOKED);
@@ -645,7 +647,7 @@ void shouldThrowConflictExceptionOn409(WireMockRuntimeInfo wmRuntimeInfo) {
.version("1.0.0")
.agentHost("test-agent.example.com")
.addEndpointsItem(new AgentEndpoint()
- .protocol(AgentEndpoint.ProtocolEnum.A2_A)
+ .protocol(Protocol.A2_A)
.agentUrl(URI.create("https://test-agent.example.com/a2a")))
.identityCsrPEM("test-csr")
.serverCsrPEM("test-csr");
@@ -699,7 +701,7 @@ void shouldThrowWhenRegistrationMissingSelfLink(WireMockRuntimeInfo wmRuntimeInf
.version("1.0.0")
.agentHost("test-agent.example.com")
.addEndpointsItem(new AgentEndpoint()
- .protocol(AgentEndpoint.ProtocolEnum.A2_A)
+ .protocol(Protocol.A2_A)
.agentUrl(URI.create("https://test-agent.example.com/a2a")))
.identityCsrPEM("test-csr")
.serverCsrPEM("test-csr");
@@ -731,7 +733,7 @@ void shouldThrowWhenRegistrationHasNullLinks(WireMockRuntimeInfo wmRuntimeInfo)
.version("1.0.0")
.agentHost("test-agent.example.com")
.addEndpointsItem(new AgentEndpoint()
- .protocol(AgentEndpoint.ProtocolEnum.A2_A)
+ .protocol(Protocol.A2_A)
.agentUrl(URI.create("https://test-agent.example.com/a2a")))
.identityCsrPEM("test-csr")
.serverCsrPEM("test-csr");
From b4cd776cc2a1df4ddee6172ab55eacdb83987794 Mon Sep 17 00:00:00 2001
From: James Hateley
Date: Thu, 23 Jul 2026 13:18:50 +1000
Subject: [PATCH 2/4] docs(api): update api-spec sources
Signed-off-by: James Hateley
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 678845b..32e0287 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,9 @@ Java SDK for the Agent Name Service (ANS) Registry. This SDK provides clients fo
## API Specification Reference
-The ANS Registry SDK is based off of the REST API. The spec is documented using the OpenAPI (Swagger) specification:
-- [View OpenAPI Spec - Human Readable](https://developer.godaddy.com/doc/endpoint/ans)
-- [OpenAPI Spec - AI/Machine Readable](https://developer.godaddy.com/swagger/swagger_ans.json)
+The ANS Registry SDK is based off of the REST API. The spec is documented using the OpenAPI specification:
+- [ANS OpenAPI Spec (ans repo)](https://github.com/agentnameservice/ans/blob/main/spec/api-spec-v2.yaml)
+- Local copy used for code generation: [`ans-sdk-api/spec/api-spec.yaml`](ans-sdk-api/spec/api-spec.yaml)
## Requirements
From 8b409e90715f4903d77c672b38ceeb8378cb41d5 Mon Sep 17 00:00:00 2001
From: James Hateley
Date: Fri, 24 Jul 2026 15:56:29 +1000
Subject: [PATCH 3/4] feat(ref-imp): add apiVersion configuration to client
Signed-off-by: James Hateley
---
.../ans/sdk/config/AnsConfiguration.java | 27 ++-
.../godaddy/ans/sdk/config/ApiVersion.java | 20 ++
.../ans/sdk/config/AnsConfigurationTest.java | 39 +++-
.../sdk/discovery/AgentCapabilityRequest.java | 23 +-
.../ans/sdk/discovery/ResolutionService.java | 11 +-
.../ans/sdk/registration/AgentPaths.java | 59 +++++
.../ans/sdk/registration/AnsApiClient.java | 31 +++
.../sdk/registration/CertificateService.java | 14 +-
.../sdk/registration/RegistrationClient.java | 21 +-
.../sdk/registration/RegistrationService.java | 55 +++--
.../registration/ApiVersionRoutingTest.java | 212 ++++++++++++++++++
.../registration/CertificateServiceTest.java | 87 +++++--
.../registration/RegistrationClientTest.java | 90 +++++---
.../examples/springboot/AgentController.java | 4 +-
14 files changed, 593 insertions(+), 100 deletions(-)
create mode 100644 ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/ApiVersion.java
create mode 100644 ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AgentPaths.java
create mode 100644 ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/ApiVersionRoutingTest.java
diff --git a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java
index 6764baa..8d88576 100644
--- a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java
+++ b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java
@@ -32,6 +32,7 @@ public final class AnsConfiguration {
private final Duration connectTimeout;
private final Duration readTimeout;
private final int maxRetries;
+ private final ApiVersion apiVersion;
private AnsConfiguration(Builder builder) {
this.environment = builder.environment;
@@ -41,6 +42,7 @@ private AnsConfiguration(Builder builder) {
this.connectTimeout = builder.connectTimeout != null ? builder.connectTimeout : DEFAULT_CONNECT_TIMEOUT;
this.readTimeout = builder.readTimeout != null ? builder.readTimeout : DEFAULT_READ_TIMEOUT;
this.maxRetries = builder.maxRetries;
+ this.apiVersion = Objects.requireNonNull(builder.apiVersion, "Api version is required");
}
/**
@@ -115,6 +117,15 @@ public boolean isRetryEnabled() {
return maxRetries > 0;
}
+ /**
+ * Returns the selected API version lane.
+ *
+ * @return the API version (defaults to {@link ApiVersion#V2})
+ */
+ public ApiVersion getApiVersion() {
+ return apiVersion;
+ }
+
/**
* Builder for {@link AnsConfiguration}.
*/
@@ -126,6 +137,7 @@ public static final class Builder {
private Duration connectTimeout;
private Duration readTimeout;
private int maxRetries = DEFAULT_MAX_RETRIES;
+ private ApiVersion apiVersion = ApiVersion.V2;
private Builder() {
}
@@ -199,6 +211,17 @@ public Builder enableRetry(int maxRetries) {
return this;
}
+ /**
+ * Sets the API version lane. Defaults to {@link ApiVersion#V2}.
+ *
+ * @param apiVersion the API version
+ * @return this builder
+ */
+ public Builder apiVersion(ApiVersion apiVersion) {
+ this.apiVersion = Objects.requireNonNull(apiVersion, "API version cannot be null");
+ return this;
+ }
+
/**
* Builds the configuration.
*
@@ -206,8 +229,8 @@ public Builder enableRetry(int maxRetries) {
* @throws NullPointerException if required fields are not set
*/
public AnsConfiguration build() {
- if (this.environment == null) {
- throw new IllegalStateException("Environment is required");
+ if (this.environment == null && this.baseUrl == null) {
+ throw new IllegalStateException("Either an environment or a base URL is required");
}
return new AnsConfiguration(this);
}
diff --git a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/ApiVersion.java b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/ApiVersion.java
new file mode 100644
index 0000000..00b76a8
--- /dev/null
+++ b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/ApiVersion.java
@@ -0,0 +1,20 @@
+package com.godaddy.ans.sdk.config;
+
+/**
+ * Selects which ANS API lane the SDK targets.
+ *
+ * New clients default to {@link #V2}. {@link #V1} remains available as an
+ * explicit opt-in for existing integrators, since both API lanes stay live.
+ */
+public enum ApiVersion {
+
+ /**
+ * Legacy {@code /v1/agents} API lane.
+ */
+ V1,
+
+ /**
+ * Current {@code /v2/ans/agents} API lane (default for new clients).
+ */
+ V2
+}
diff --git a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java
index ff14821..018673e 100644
--- a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java
+++ b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java
@@ -115,13 +115,13 @@ void shouldThrowExceptionWhenCredentialsProviderIsNull() {
}
@Test
- @DisplayName("Should throw when environment is not set")
+ @DisplayName("Should throw when neither environment nor base URL is set")
void shouldThrowWhenEnvironmentNotSet() {
assertThatThrownBy(() -> AnsConfiguration.builder()
.credentialsProvider(testProvider)
.build())
.isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Environment is required");
+ .hasMessageContaining("Either an environment or a base URL is required");
}
@Test
@@ -147,4 +147,39 @@ void shouldReturnCredentialsProvider() {
assertThat(config.getCredentialsProvider()).isSameAs(testProvider);
}
+
+ @Test
+ @DisplayName("Should default API version to V2")
+ void shouldDefaultApiVersionToV2() {
+ AnsConfiguration config = AnsConfiguration.builder()
+ .environment(Environment.OTE)
+ .credentialsProvider(testProvider)
+ .build();
+
+ assertThat(config.getApiVersion()).isEqualTo(ApiVersion.V2);
+ }
+
+ @Test
+ @DisplayName("Should override API version to V1")
+ void shouldOverrideApiVersionToV1() {
+ AnsConfiguration config = AnsConfiguration.builder()
+ .environment(Environment.OTE)
+ .credentialsProvider(testProvider)
+ .apiVersion(ApiVersion.V1)
+ .build();
+
+ assertThat(config.getApiVersion()).isEqualTo(ApiVersion.V1);
+ }
+
+ @Test
+ @DisplayName("Should throw when API version is null")
+ void shouldThrowWhenApiVersionNull() {
+ assertThatThrownBy(() -> AnsConfiguration.builder()
+ .environment(Environment.OTE)
+ .credentialsProvider(testProvider)
+ .apiVersion(null)
+ .build())
+ .isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("API version");
+ }
}
\ No newline at end of file
diff --git a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java
index 946c76e..d685b5a 100644
--- a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java
+++ b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/AgentCapabilityRequest.java
@@ -7,26 +7,5 @@
* v1-only endpoint, so this local DTO fills in for the removed
* {@code AgentCapabilityRequest} model.
*/
-final class AgentCapabilityRequest {
-
- private String agentHost;
- private String version;
-
- AgentCapabilityRequest agentHost(String agentHost) {
- this.agentHost = agentHost;
- return this;
- }
-
- AgentCapabilityRequest version(String version) {
- this.version = version;
- return this;
- }
-
- public String getAgentHost() {
- return agentHost;
- }
-
- public String getVersion() {
- return version;
- }
+record AgentCapabilityRequest(String agentHost, String version) {
}
\ No newline at end of file
diff --git a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java
index fb49e24..8682a5d 100644
--- a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java
+++ b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/ResolutionService.java
@@ -20,6 +20,13 @@
/**
* Internal service for handling agent resolution API calls.
+ *
+ * Resolution is intentionally pinned to the legacy-live v1 service
+ * ({@code /v1/agents/resolution} and {@code /v1/agents/{agentId}}) regardless of
+ * the configured {@link com.godaddy.ans.sdk.config.ApiVersion}. Resolution has no
+ * v2 equivalent: {@code POST /v1/agents/resolution} was deliberately not carried
+ * forward to v2 (the {@code _ans} DNS TXT record already carries agent endpoints).
+ * This pinning is by design, not an oversight.
*/
class ResolutionService {
@@ -47,9 +54,7 @@ class ResolutionService {
AgentDetails resolve(String agentHost, String version) {
// Step 1: POST to /v1/agents/resolution to get the agent-details link
String resolveVersion = (version != null && !version.isEmpty()) ? version : "*";
- AgentCapabilityRequest request = new AgentCapabilityRequest()
- .agentHost(agentHost)
- .version(resolveVersion);
+ AgentCapabilityRequest request = new AgentCapabilityRequest(agentHost, resolveVersion);
String requestBody = serializeToJson(request);
HttpRequest resolutionRequest = createRequestBuilder("/v1/agents/resolution")
diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AgentPaths.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AgentPaths.java
new file mode 100644
index 0000000..2b5ede0
--- /dev/null
+++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AgentPaths.java
@@ -0,0 +1,59 @@
+package com.godaddy.ans.sdk.registration;
+
+import com.godaddy.ans.sdk.config.ApiVersion;
+
+/**
+ * Builds ANS agent API paths for the selected {@link ApiVersion} lane.
+ *
+ * All version-dependent path branching lives here so registration and
+ * certificate services never string-concatenate lane-specific paths inline.
+ */
+final class AgentPaths {
+
+ private static final String V2_COLLECTION = "/v2/ans/agents";
+ private static final String V1_COLLECTION = "/v1/agents";
+
+ private AgentPaths() {
+ }
+
+ /**
+ * Returns the agents collection path for the given lane.
+ *
+ * @param apiVersion the selected API version
+ * @return {@code /v2/ans/agents} for v2, {@code /v1/agents} for v1
+ */
+ static String agentsCollectionPath(ApiVersion apiVersion) {
+ return apiVersion == ApiVersion.V1 ? V1_COLLECTION : V2_COLLECTION;
+ }
+
+ /**
+ * Returns the registration path for the given lane.
+ *
+ * v2 registers via a standard REST collection POST; v1 uses the legacy
+ * {@code /register} verb suffix.
+ *
+ * @param apiVersion the selected API version
+ * @return {@code /v2/ans/agents} for v2, {@code /v1/agents/register} for v1
+ */
+ static String registerPath(ApiVersion apiVersion) {
+ return apiVersion == ApiVersion.V1 ? V1_COLLECTION + "/register" : V2_COLLECTION;
+ }
+
+ /**
+ * Builds an agent-scoped path: collection + agentId + any trailing segments.
+ *
+ * @param apiVersion the selected API version
+ * @param agentId the agent ID
+ * @param segments optional trailing path segments (e.g. {@code "certificates", "identity"})
+ * @return the joined path, e.g. {@code /v2/ans/agents/{agentId}/certificates/identity}
+ */
+ static String agentPath(ApiVersion apiVersion, String agentId, String... segments) {
+ StringBuilder path = new StringBuilder(agentsCollectionPath(apiVersion))
+ .append('/')
+ .append(agentId);
+ for (String segment : segments) {
+ path.append('/').append(segment);
+ }
+ return path.toString();
+ }
+}
diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AnsApiClient.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AnsApiClient.java
index 2eb6f91..2fa8b5d 100644
--- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AnsApiClient.java
+++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/AnsApiClient.java
@@ -2,10 +2,13 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.godaddy.ans.sdk.auth.AnsCredentials;
import com.godaddy.ans.sdk.config.AnsConfiguration;
+import com.godaddy.ans.sdk.config.ApiVersion;
import com.godaddy.ans.sdk.exception.AnsAuthenticationException;
import com.godaddy.ans.sdk.exception.AnsConflictException;
import com.godaddy.ans.sdk.exception.AnsNotFoundException;
@@ -164,4 +167,32 @@ String serializeToJson(Object object) {
throw new AnsServerException("Failed to serialize request: " + e.getMessage(), 0, e, null);
}
}
+
+ /**
+ * Serializes an object to JSON with the named top-level field removed.
+ *
+ * Used to strip v2-only fields (e.g. {@code discoveryProfiles}) from the
+ * wire body when targeting the v1 lane, without mutating the caller's object.
+ * Operates on the JSON tree and reuses this client's {@link ObjectMapper}.
+ *
+ * @param object the object to serialize
+ * @param fieldName the top-level field to remove
+ * @return the JSON string without the named field
+ * @throws AnsServerException if serialization fails
+ */
+ String serializeToJsonWithoutField(Object object, String fieldName) {
+ try {
+ JsonNode tree = objectMapper.readTree(objectMapper.writeValueAsString(object));
+ if (tree instanceof ObjectNode objectNode) {
+ objectNode.remove(fieldName);
+ }
+ return objectMapper.writeValueAsString(tree);
+ } catch (IOException e) {
+ throw new AnsServerException("Failed to serialize request: " + e.getMessage(), 0, e, null);
+ }
+ }
+
+ ApiVersion getApiVersion() {
+ return configuration.getApiVersion();
+ }
}
diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/CertificateService.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/CertificateService.java
index 71316d1..ca317c3 100644
--- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/CertificateService.java
+++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/CertificateService.java
@@ -1,7 +1,7 @@
package com.godaddy.ans.sdk.registration;
import com.fasterxml.jackson.core.type.TypeReference;
-import com.godaddy.ans.sdk.config.AnsConfiguration;
+import com.godaddy.ans.sdk.config.ApiVersion;
import com.godaddy.ans.sdk.exception.AnsAuthenticationException;
import com.godaddy.ans.sdk.exception.AnsNotFoundException;
import com.godaddy.ans.sdk.exception.AnsServerException;
@@ -34,9 +34,11 @@
public class CertificateService {
private final AnsApiClient httpClient;
+ private final ApiVersion apiVersion;
- CertificateService(AnsConfiguration configuration) {
- this.httpClient = new AnsApiClient(configuration);
+ CertificateService(AnsApiClient ansApiClient) {
+ this.httpClient = ansApiClient;
+ this.apiVersion = ansApiClient.getApiVersion();
}
/**
@@ -55,7 +57,8 @@ public class CertificateService {
public List getIdentityCertificates(String agentId) {
validateAgentId(agentId);
- HttpRequest request = httpClient.createRequestBuilder("/v1/agents/" + agentId + "/certificates/identity")
+ HttpRequest request = httpClient.createRequestBuilder(
+ AgentPaths.agentPath(apiVersion, agentId, "certificates", "identity"))
.GET()
.build();
@@ -79,7 +82,8 @@ public List getIdentityCertificates(String agentId) {
public List getServerCertificates(String agentId) {
validateAgentId(agentId);
- HttpRequest request = httpClient.createRequestBuilder("/v1/agents/" + agentId + "/certificates/server")
+ HttpRequest request = httpClient.createRequestBuilder(
+ AgentPaths.agentPath(apiVersion, agentId, "certificates", "server"))
.GET()
.build();
diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java
index 6ebfbf8..23da29b 100644
--- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java
+++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java
@@ -3,6 +3,7 @@
import com.godaddy.ans.sdk.auth.AnsCredentialsProvider;
import com.godaddy.ans.sdk.concurrent.AnsExecutors;
import com.godaddy.ans.sdk.config.AnsConfiguration;
+import com.godaddy.ans.sdk.config.ApiVersion;
import com.godaddy.ans.sdk.config.Environment;
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import com.godaddy.ans.sdk.model.generated.AgentRegistrationRequest;
@@ -55,11 +56,10 @@ public final class RegistrationClient {
private final RegistrationService registrationService;
private final CertificateService certificateService;
- private RegistrationClient(AnsConfiguration configuration) {
+ private RegistrationClient(AnsConfiguration configuration, AnsApiClient ansApiClient) {
this.configuration = configuration;
- var ansClient = new AnsApiClient(configuration);
- this.registrationService = new RegistrationService(ansClient);
- this.certificateService = new CertificateService(configuration);
+ this.registrationService = new RegistrationService(ansApiClient);
+ this.certificateService = new CertificateService(ansApiClient);
}
/**
@@ -338,6 +338,17 @@ public Builder enableRetry(int maxRetries) {
return this;
}
+ /**
+ * Sets the API version lane. Defaults to {@link ApiVersion#V2}.
+ *
+ * @param apiVersion the API version
+ * @return this builder
+ */
+ public Builder apiVersion(ApiVersion apiVersion) {
+ configBuilder.apiVersion(apiVersion);
+ return this;
+ }
+
/**
* Builds the RegistrationClient.
*
@@ -347,7 +358,7 @@ public RegistrationClient build() {
AnsConfiguration config = (prebuiltConfiguration != null)
? prebuiltConfiguration
: configBuilder.build();
- return new RegistrationClient(config);
+ return new RegistrationClient(config, new AnsApiClient(config));
}
}
}
\ No newline at end of file
diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java
index a360491..9bddbaa 100644
--- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java
+++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java
@@ -1,5 +1,6 @@
package com.godaddy.ans.sdk.registration;
+import com.godaddy.ans.sdk.config.ApiVersion;
import com.godaddy.ans.sdk.exception.AnsServerException;
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import com.godaddy.ans.sdk.model.generated.AgentRegistrationRequest;
@@ -11,6 +12,7 @@
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.util.UUID;
/**
* Internal service for handling registration API calls.
@@ -18,33 +20,55 @@
class RegistrationService {
private final AnsApiClient httpClient;
+ private final ApiVersion apiVersion;
RegistrationService(final AnsApiClient ansApiClient) {
this.httpClient = ansApiClient;
+ this.apiVersion = ansApiClient.getApiVersion();
}
+
/**
* Registers a new agent and returns full agent details.
*
- * This method registers the agent and then follows the 'self' link
- * to retrieve the complete AgentDetails including the agentId.
+ * On the v2 lane the {@code agentId} returned in the registration response
+ * is used directly to fetch the complete {@link AgentDetails}. On the v1 lane
+ * the 'self' link is followed instead (HATEOAS).
*/
AgentDetails register(AgentRegistrationRequest request) {
- String requestBody = httpClient.serializeToJson(request);
+ // discoveryProfiles is a v2-only field; strip it from the wire body on v1.
+ String requestBody = (apiVersion == ApiVersion.V1)
+ ? httpClient.serializeToJsonWithoutField(request, "discoveryProfiles")
+ : httpClient.serializeToJson(request);
- HttpRequest httpRequest = httpClient.createRequestBuilder("/v1/agents/register")
+ HttpRequest httpRequest = httpClient.createRequestBuilder(AgentPaths.registerPath(apiVersion))
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse response = httpClient.sendRequest(httpRequest);
RegistrationPending pending = httpClient.parseResponse(response.body(), RegistrationPending.class);
- // Follow the 'self' link to get full AgentDetails with agentId
- String selfPath = extractSelfLink(pending);
- if (selfPath == null) {
- throw new AnsServerException("Registration response missing 'self' link", 0, null);
+ return getAgentDetails(resolveAgentDetailsPath(pending));
+ }
+
+ /**
+ * Resolves the path to fetch full agent details after registration.
+ *
+ * v2 uses {@code pending.getAgentId()} directly; v1 follows the 'self' link.
+ */
+ private String resolveAgentDetailsPath(RegistrationPending pending) {
+ if (apiVersion == ApiVersion.V1) {
+ String selfPath = extractSelfLink(pending);
+ if (selfPath == null) {
+ throw new AnsServerException("Registration response missing 'self' link", 0, null);
+ }
+ return selfPath;
}
- return getAgentDetails(selfPath);
+ UUID agentId = pending.getAgentId();
+ if (agentId == null) {
+ throw new AnsServerException("Registration response missing 'agentId'", 0, null);
+ }
+ return AgentPaths.agentPath(apiVersion, agentId.toString());
}
/**
@@ -63,14 +87,15 @@ AgentDetails getAgentDetails(String path) {
* Gets agent details by agent ID.
*/
AgentDetails getAgent(String agentId) {
- return getAgentDetails("/v1/agents/" + agentId);
+ return getAgentDetails(AgentPaths.agentPath(apiVersion, agentId));
}
/**
* Triggers ACME verification.
*/
AgentStatus verifyAcme(String agentId) {
- HttpRequest request = httpClient.createRequestBuilder("/v1/agents/" + agentId + "/verify-acme")
+ HttpRequest request = httpClient.createRequestBuilder(
+ AgentPaths.agentPath(apiVersion, agentId, "verify-acme"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
@@ -82,7 +107,8 @@ AgentStatus verifyAcme(String agentId) {
* Triggers DNS verification.
*/
AgentStatus verifyDns(String agentId) {
- HttpRequest request = httpClient.createRequestBuilder("/v1/agents/" + agentId + "/verify-dns")
+ HttpRequest request = httpClient.createRequestBuilder(
+ AgentPaths.agentPath(apiVersion, agentId, "verify-dns"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
@@ -104,7 +130,8 @@ AgentStatus verifyDns(String agentId) {
AgentRevocationResponse revoke(String agentId, AgentRevocationRequest request) {
String requestBody = httpClient.serializeToJson(request);
- HttpRequest httpRequest = httpClient.createRequestBuilder("/v1/agents/" + agentId + "/revoke")
+ HttpRequest httpRequest = httpClient.createRequestBuilder(
+ AgentPaths.agentPath(apiVersion, agentId, "revoke"))
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
@@ -120,7 +147,7 @@ private String extractSelfLink(RegistrationPending pending) {
return null;
}
for (Link link : pending.getLinks()) {
- if ("self".equals(link.getRel()) && link.getHref() != null) {
+ if ("self".equals(link.getRel())) {
return link.getHref().getPath();
}
}
diff --git a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/ApiVersionRoutingTest.java b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/ApiVersionRoutingTest.java
new file mode 100644
index 0000000..926500b
--- /dev/null
+++ b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/ApiVersionRoutingTest.java
@@ -0,0 +1,212 @@
+package com.godaddy.ans.sdk.registration;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import com.godaddy.ans.sdk.auth.JwtCredentialsProvider;
+import com.godaddy.ans.sdk.config.ApiVersion;
+import com.godaddy.ans.sdk.config.Environment;
+import com.godaddy.ans.sdk.model.generated.AgentEndpoint;
+import com.godaddy.ans.sdk.model.generated.AgentRegistrationRequest;
+import com.godaddy.ans.sdk.model.generated.AgentRevocationRequest;
+import com.godaddy.ans.sdk.model.generated.Protocol;
+import com.godaddy.ans.sdk.model.generated.RevocationReason;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.absent;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies API-version path routing across the v1 and v2 lanes.
+ *
+ * The v2 default lane is exercised by {@link RegistrationClientTest}; this
+ * class pins the client to {@link ApiVersion#V1} and asserts the legacy
+ * {@code /v1/agents/...} paths plus v1-specific wire-body behavior.
+ */
+@WireMockTest
+class ApiVersionRoutingTest {
+
+ private static final String TEST_JWT_TOKEN = "test-jwt-token";
+ private static final String TEST_AGENT_ID = "550e8400-e29b-41d4-a716-446655440000";
+
+ private RegistrationClient v1Client(WireMockRuntimeInfo wm) {
+ return RegistrationClient.builder()
+ .environment(Environment.OTE)
+ .baseUrl(wm.getHttpBaseUrl())
+ .apiVersion(ApiVersion.V1)
+ .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
+ .build();
+ }
+
+ private AgentRegistrationRequest sampleRequest() {
+ return new AgentRegistrationRequest()
+ .agentDisplayName("Test Agent")
+ .version("1.0.0")
+ .agentHost("test-agent.example.com")
+ .addEndpointsItem(new AgentEndpoint()
+ .protocol(Protocol.A2_A)
+ .agentUrl(URI.create("https://test-agent.example.com/a2a")))
+ .identityCsrPEM("test-csr")
+ .serverCsrPEM("test-csr");
+ }
+
+ @Test
+ @DisplayName("v1 register posts to /v1/agents/register and follows the self link")
+ void v1RegisterFollowsSelfLink(WireMockRuntimeInfo wm) {
+ stubFor(post(urlEqualTo("/v1/agents/register"))
+ .willReturn(aResponse()
+ .withStatus(202)
+ .withHeader("Content-Type", "application/json")
+ .withBody(v1PendingWithSelfLink())));
+
+ stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(agentDetails())));
+
+ assertThat(v1Client(wm).registerAgent(sampleRequest())).isNotNull();
+
+ verify(postRequestedFor(urlEqualTo("/v1/agents/register")));
+ }
+
+ @Test
+ @DisplayName("v1 register omits the v2-only discoveryProfiles field from the wire body")
+ void v1RegisterStripsDiscoveryProfiles(WireMockRuntimeInfo wm) {
+ stubFor(post(urlEqualTo("/v1/agents/register"))
+ .willReturn(aResponse()
+ .withStatus(202)
+ .withHeader("Content-Type", "application/json")
+ .withBody(v1PendingWithSelfLink())));
+
+ stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(agentDetails())));
+
+ v1Client(wm).registerAgent(sampleRequest());
+
+ verify(postRequestedFor(urlEqualTo("/v1/agents/register"))
+ .withRequestBody(containing("\"agentDisplayName\":\"Test Agent\""))
+ .withRequestBody(matchingJsonPath("$.discoveryProfiles", absent())));
+ }
+
+ @Test
+ @DisplayName("v1 getAgent targets /v1/agents/{agentId}")
+ void v1GetAgentPath(WireMockRuntimeInfo wm) {
+ stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(agentDetails())));
+
+ assertThat(v1Client(wm).getAgent(TEST_AGENT_ID)).isNotNull();
+ }
+
+ @Test
+ @DisplayName("v1 verifyAcme targets /v1/agents/{agentId}/verify-acme")
+ void v1VerifyAcmePath(WireMockRuntimeInfo wm) {
+ stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-acme"))
+ .willReturn(aResponse()
+ .withStatus(202)
+ .withHeader("Content-Type", "application/json")
+ .withBody(agentStatus())));
+
+ assertThat(v1Client(wm).verifyAcme(TEST_AGENT_ID)).isNotNull();
+ }
+
+ @Test
+ @DisplayName("v1 verifyDns targets /v1/agents/{agentId}/verify-dns")
+ void v1VerifyDnsPath(WireMockRuntimeInfo wm) {
+ stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-dns"))
+ .willReturn(aResponse()
+ .withStatus(202)
+ .withHeader("Content-Type", "application/json")
+ .withBody(agentStatus())));
+
+ assertThat(v1Client(wm).verifyDns(TEST_AGENT_ID)).isNotNull();
+ }
+
+ @Test
+ @DisplayName("v1 revoke targets /v1/agents/{agentId}/revoke")
+ void v1RevokePath(WireMockRuntimeInfo wm) {
+ stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(revocation())));
+
+ AgentRevocationRequest request = new AgentRevocationRequest()
+ .reason(RevocationReason.CESSATION_OF_OPERATION);
+
+ assertThat(v1Client(wm).revokeAgent(TEST_AGENT_ID, request)).isNotNull();
+ }
+
+ // ==================== Response bodies ====================
+
+ private String v1PendingWithSelfLink() {
+ return """
+ {
+ "status": "PENDING_VALIDATION",
+ "ansName": "ans://v1.0.0.test-agent.example.com",
+ "links": [
+ { "rel": "self", "href": "/v1/agents/550e8400-e29b-41d4-a716-446655440000" }
+ ]
+ }
+ """;
+ }
+
+ private String agentDetails() {
+ return """
+ {
+ "agentId": "550e8400-e29b-41d4-a716-446655440000",
+ "agentDisplayName": "Test Agent",
+ "version": "1.0.0",
+ "agentHost": "test-agent.example.com",
+ "ansName": "ans://v1.0.0.test-agent.example.com",
+ "agentStatus": "PENDING_VALIDATION",
+ "endpoints": [
+ { "protocol": "A2A", "agentUrl": "https://test-agent.example.com/a2a" }
+ ]
+ }
+ """;
+ }
+
+ private String agentStatus() {
+ return """
+ {
+ "status": "PENDING_DNS",
+ "phase": "DOMAIN_VALIDATION",
+ "completedSteps": ["REGISTRATION_SUBMITTED"],
+ "pendingSteps": ["DNS_VERIFICATION"],
+ "createdAt": "2024-01-15T10:00:00Z",
+ "updatedAt": "2024-01-15T10:30:00Z"
+ }
+ """;
+ }
+
+ private String revocation() {
+ return """
+ {
+ "agentId": "550e8400-e29b-41d4-a716-446655440000",
+ "ansName": "ans://v1.0.0.test-agent.example.com",
+ "status": "REVOKED",
+ "revokedAt": "2024-01-15T12:00:00Z",
+ "reason": "CESSATION_OF_OPERATION",
+ "dnsRecordsToRemove": []
+ }
+ """;
+ }
+}
diff --git a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java
index f566b3d..16a41b6 100644
--- a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java
+++ b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java
@@ -4,6 +4,7 @@
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.godaddy.ans.sdk.auth.ApiKeyCredentialsProvider;
import com.godaddy.ans.sdk.config.AnsConfiguration;
+import com.godaddy.ans.sdk.config.ApiVersion;
import com.godaddy.ans.sdk.config.Environment;
import com.godaddy.ans.sdk.exception.AnsAuthenticationException;
import com.godaddy.ans.sdk.exception.AnsNotFoundException;
@@ -41,7 +42,51 @@ private CertificateService createCertificateService(WireMockRuntimeInfo wmRuntim
.baseUrl(wmRuntimeInfo.getHttpBaseUrl())
.credentialsProvider(new ApiKeyCredentialsProvider(API_KEY, API_SECRET))
.build();
- return new CertificateService(config);
+ return new CertificateService(new AnsApiClient(config));
+ }
+
+ private CertificateService createV1CertificateService(WireMockRuntimeInfo wmRuntimeInfo) {
+ AnsConfiguration config = AnsConfiguration.builder()
+ .environment(Environment.OTE)
+ .baseUrl(wmRuntimeInfo.getHttpBaseUrl())
+ .apiVersion(ApiVersion.V1)
+ .credentialsProvider(new ApiKeyCredentialsProvider(API_KEY, API_SECRET))
+ .build();
+ return new CertificateService(new AnsApiClient(config));
+ }
+
+ // ==================== API version routing ====================
+
+ @Test
+ @DisplayName("v1 lane targets /v1/agents/{id}/certificates/server")
+ void v1ServerCertificatesPath(WireMockRuntimeInfo wmRuntimeInfo) {
+ stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[]")));
+
+ List certs = createV1CertificateService(wmRuntimeInfo)
+ .getServerCertificates(TEST_AGENT_ID);
+
+ assertThat(certs).isEmpty();
+ verify(getRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server")));
+ }
+
+ @Test
+ @DisplayName("v1 lane targets /v1/agents/{id}/certificates/identity")
+ void v1IdentityCertificatesPath(WireMockRuntimeInfo wmRuntimeInfo) {
+ stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[]")));
+
+ List certs = createV1CertificateService(wmRuntimeInfo)
+ .getIdentityCertificates(TEST_AGENT_ID);
+
+ assertThat(certs).isEmpty();
+ verify(getRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity")));
}
// ==================== getServerCertificates Tests ====================
@@ -67,7 +112,7 @@ void getServerCertificatesShouldReturnListOnSuccess(WireMockRuntimeInfo wmRuntim
]
""";
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -85,7 +130,7 @@ void getServerCertificatesShouldReturnListOnSuccess(WireMockRuntimeInfo wmRuntim
assertThat(cert.getCertificateIssuer()).isEqualTo("CN=ANS CA");
assertThat(cert.getCertificatePEM()).startsWith("-----BEGIN CERTIFICATE-----");
- verify(getRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ verify(getRequestedFor(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.withHeader("Authorization", containing("sso-key")));
}
@@ -93,7 +138,7 @@ void getServerCertificatesShouldReturnListOnSuccess(WireMockRuntimeInfo wmRuntim
@DisplayName("getServerCertificates should return empty list when no certificates exist")
void getServerCertificatesShouldReturnEmptyListWhenNoCertificates(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -112,7 +157,7 @@ void getServerCertificatesShouldReturnEmptyListWhenNoCertificates(WireMockRuntim
@DisplayName("getServerCertificates should throw AnsNotFoundException when agent not found")
void getServerCertificatesShouldThrowNotFoundWhenAgentNotFound(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
@@ -148,7 +193,7 @@ void getServerCertificatesShouldReturnMultipleCertificates(WireMockRuntimeInfo w
]
""";
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -185,7 +230,7 @@ void getIdentityCertificatesShouldReturnListOnSuccess(WireMockRuntimeInfo wmRunt
]
""";
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -202,7 +247,7 @@ void getIdentityCertificatesShouldReturnListOnSuccess(WireMockRuntimeInfo wmRunt
assertThat(cert.getCertificateSubject()).isEqualTo("CN=test-agent.example.com");
assertThat(cert.getCertificateIssuer()).isEqualTo("CN=ANS Identity CA");
- verify(getRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ verify(getRequestedFor(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.withHeader("Authorization", containing("sso-key")));
}
@@ -210,7 +255,7 @@ void getIdentityCertificatesShouldReturnListOnSuccess(WireMockRuntimeInfo wmRunt
@DisplayName("getIdentityCertificates should return empty list when no certificates exist")
void getIdentityCertificatesShouldReturnEmptyListWhenNoCertificates(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -229,7 +274,7 @@ void getIdentityCertificatesShouldReturnEmptyListWhenNoCertificates(WireMockRunt
@DisplayName("getIdentityCertificates should throw AnsNotFoundException when agent not found")
void getIdentityCertificatesShouldThrowNotFoundWhenAgentNotFound(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
@@ -290,7 +335,7 @@ void getIdentityCertificatesShouldRejectBlankAgentId(WireMockRuntimeInfo wmRunti
@DisplayName("getServerCertificates should throw AnsAuthenticationException on 401")
void getServerCertificatesShouldThrowAuthExceptionOn401(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(401)
.withHeader("Content-Type", "application/json")
@@ -309,7 +354,7 @@ void getServerCertificatesShouldThrowAuthExceptionOn401(WireMockRuntimeInfo wmRu
@DisplayName("getServerCertificates should throw AnsAuthenticationException on 403")
void getServerCertificatesShouldThrowAuthExceptionOn403(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(403)
.withHeader("Content-Type", "application/json")
@@ -327,7 +372,7 @@ void getServerCertificatesShouldThrowAuthExceptionOn403(WireMockRuntimeInfo wmRu
@DisplayName("getIdentityCertificates should throw AnsAuthenticationException on 401")
void getIdentityCertificatesShouldThrowAuthExceptionOn401(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.willReturn(aResponse()
.withStatus(401)
.withHeader("Content-Type", "application/json")
@@ -346,7 +391,7 @@ void getIdentityCertificatesShouldThrowAuthExceptionOn401(WireMockRuntimeInfo wm
@DisplayName("getServerCertificates should throw AnsValidationException on 422")
void getServerCertificatesShouldThrowValidationExceptionOn422(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(422)
.withHeader("Content-Type", "application/json")
@@ -364,7 +409,7 @@ void getServerCertificatesShouldThrowValidationExceptionOn422(WireMockRuntimeInf
@DisplayName("getIdentityCertificates should throw AnsValidationException on 422")
void getIdentityCertificatesShouldThrowValidationExceptionOn422(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.willReturn(aResponse()
.withStatus(422)
.withHeader("Content-Type", "application/json")
@@ -383,7 +428,7 @@ void getIdentityCertificatesShouldThrowValidationExceptionOn422(WireMockRuntimeI
@DisplayName("getServerCertificates should throw AnsServerException on 500")
void getServerCertificatesShouldThrowServerExceptionOn500(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(500)
.withHeader("Content-Type", "application/json")
@@ -402,7 +447,7 @@ void getServerCertificatesShouldThrowServerExceptionOn500(WireMockRuntimeInfo wm
@DisplayName("getServerCertificates should throw AnsServerException on 502")
void getServerCertificatesShouldThrowServerExceptionOn502(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(502)
.withHeader("Content-Type", "application/json")
@@ -419,7 +464,7 @@ void getServerCertificatesShouldThrowServerExceptionOn502(WireMockRuntimeInfo wm
@DisplayName("getServerCertificates should throw AnsServerException on 503")
void getServerCertificatesShouldThrowServerExceptionOn503(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(503)
.withHeader("Content-Type", "application/json")
@@ -436,7 +481,7 @@ void getServerCertificatesShouldThrowServerExceptionOn503(WireMockRuntimeInfo wm
@DisplayName("getIdentityCertificates should throw AnsServerException on 500")
void getIdentityCertificatesShouldThrowServerExceptionOn500(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/identity"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/identity"))
.willReturn(aResponse()
.withStatus(500)
.withHeader("Content-Type", "application/json")
@@ -453,7 +498,7 @@ void getIdentityCertificatesShouldThrowServerExceptionOn500(WireMockRuntimeInfo
@DisplayName("getServerCertificates should throw AnsServerException on unexpected 4xx error")
void getServerCertificatesShouldThrowServerExceptionOnUnexpected4xx(WireMockRuntimeInfo wmRuntimeInfo) {
// Given - 418 I'm a teapot (unexpected client error)
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(418)
.withHeader("Content-Type", "application/json")
@@ -471,7 +516,7 @@ void getServerCertificatesShouldThrowServerExceptionOnUnexpected4xx(WireMockRunt
@DisplayName("getServerCertificates should throw AnsServerException on malformed JSON response")
void getServerCertificatesShouldThrowServerExceptionOnMalformedJson(WireMockRuntimeInfo wmRuntimeInfo) {
// Given
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/certificates/server"))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/certificates/server"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
diff --git a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java
index 6bc4219..a68f396 100644
--- a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java
+++ b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java
@@ -3,6 +3,7 @@
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.godaddy.ans.sdk.auth.JwtCredentialsProvider;
+import com.godaddy.ans.sdk.config.ApiVersion;
import com.godaddy.ans.sdk.config.Environment;
import com.godaddy.ans.sdk.exception.AnsAuthenticationException;
import com.godaddy.ans.sdk.exception.AnsConflictException;
@@ -131,14 +132,14 @@ void shouldRegisterAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
// Stub the initial registration POST
- stubFor(post(urlEqualTo("/v1/agents/register"))
+ stubFor(post(urlEqualTo("/v2/ans/agents"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
.withBody(registrationPendingResponse())));
// Stub the follow-up GET for AgentDetails
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -171,8 +172,11 @@ void shouldRegisterAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
assertThat(result.getRegistrationPending().getChallenges()).hasSize(1);
assertThat(result.getRegistrationPending().getNextSteps()).hasSize(1);
- verify(postRequestedFor(urlEqualTo("/v1/agents/register"))
+ verify(postRequestedFor(urlEqualTo("/v2/ans/agents"))
.withRequestBody(containing("\"agentDisplayName\":\"Test Agent\""))
+ // discoveryProfiles is v2-only and defaults to [ANS_DNSAID] on the wire
+ .withRequestBody(containing("\"discoveryProfiles\""))
+ .withRequestBody(containing("ANS_DNSAID"))
.withHeader("Authorization", equalTo("sso-jwt " + TEST_JWT_TOKEN)));
}
@@ -181,7 +185,7 @@ void shouldRegisterAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowValidationExceptionOn422(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/register"))
+ stubFor(post(urlEqualTo("/v2/ans/agents"))
.willReturn(aResponse()
.withStatus(422)
.withHeader("Content-Type", "application/json")
@@ -216,7 +220,7 @@ void shouldThrowValidationExceptionOn422(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldVerifyAcmeSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-acme"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/verify-acme"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
@@ -239,7 +243,7 @@ void shouldVerifyAcmeSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowNotFoundExceptionForVerifyAcme(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-acme"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/verify-acme"))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
@@ -263,7 +267,7 @@ void shouldThrowNotFoundExceptionForVerifyAcme(WireMockRuntimeInfo wmRuntimeInfo
void shouldVerifyDnsSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-dns"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/verify-dns"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
@@ -286,7 +290,7 @@ void shouldVerifyDnsSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowAuthExceptionForVerifyDns(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-dns"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/verify-dns"))
.willReturn(aResponse()
.withStatus(401)
.withHeader("Content-Type", "application/json")
@@ -310,7 +314,7 @@ void shouldThrowAuthExceptionForVerifyDns(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldRevokeAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -334,7 +338,7 @@ void shouldRevokeAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
assertThat(result.getReason()).isEqualTo(RevocationReason.CESSATION_OF_OPERATION);
assertThat(result.getDnsRecordsToRemove()).hasSize(3);
- verify(postRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ verify(postRequestedFor(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.withRequestBody(containing("\"reason\":\"CESSATION_OF_OPERATION\""))
.withRequestBody(containing("\"comments\":\"Agent being decommissioned\""))
.withHeader("Authorization", equalTo("sso-jwt " + TEST_JWT_TOKEN)));
@@ -345,7 +349,7 @@ void shouldRevokeAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldRevokeAgentWithJustReason(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -363,7 +367,7 @@ void shouldRevokeAgentWithJustReason(WireMockRuntimeInfo wmRuntimeInfo) {
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(AgentLifecycleStatus.REVOKED);
- verify(postRequestedFor(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ verify(postRequestedFor(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.withRequestBody(containing("\"reason\":\"KEY_COMPROMISE\"")));
}
@@ -372,7 +376,7 @@ void shouldRevokeAgentWithJustReason(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowNotFoundExceptionForRevoke(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
@@ -395,7 +399,7 @@ void shouldThrowNotFoundExceptionForRevoke(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowValidationExceptionWhenAlreadyRevoked(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(422)
.withHeader("Content-Type", "application/json")
@@ -419,7 +423,7 @@ void shouldThrowValidationExceptionWhenAlreadyRevoked(WireMockRuntimeInfo wmRunt
void shouldThrowValidationExceptionForPendingValidation(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(422)
.withHeader("Content-Type", "application/json")
@@ -445,7 +449,7 @@ void shouldThrowValidationExceptionForPendingValidation(WireMockRuntimeInfo wmRu
void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -469,7 +473,7 @@ void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowNotFoundExceptionWhenAgentNotFound(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
@@ -493,13 +497,13 @@ void shouldThrowNotFoundExceptionWhenAgentNotFound(WireMockRuntimeInfo wmRuntime
void shouldRegisterAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/register"))
+ stubFor(post(urlEqualTo("/v2/ans/agents"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
.withBody(registrationPendingResponse())));
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -532,7 +536,7 @@ void shouldRegisterAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio
void shouldVerifyAcmeAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-acme"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/verify-acme"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
@@ -555,7 +559,7 @@ void shouldVerifyAcmeAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
void shouldVerifyDnsAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/verify-dns"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/verify-dns"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
@@ -578,7 +582,7 @@ void shouldVerifyDnsAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
void shouldRevokeAgentAsyncWithRequest(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -604,7 +608,7 @@ void shouldRevokeAgentAsyncWithRequest(WireMockRuntimeInfo wmRuntimeInfo) throws
void shouldRevokeAgentAsyncWithReason(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/" + TEST_AGENT_ID + "/revoke"))
+ stubFor(post(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID + "/revoke"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
@@ -630,7 +634,7 @@ void shouldRevokeAgentAsyncWithReason(WireMockRuntimeInfo wmRuntimeInfo) throws
void shouldThrowConflictExceptionOn409(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(post(urlEqualTo("/v1/agents/register"))
+ stubFor(post(urlEqualTo("/v2/ans/agents"))
.willReturn(aResponse()
.withStatus(409)
.withHeader("Content-Type", "application/json")
@@ -662,7 +666,7 @@ void shouldThrowConflictExceptionOn409(WireMockRuntimeInfo wmRuntimeInfo) {
void shouldThrowServerExceptionOnUnexpectedStatusCode(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
- stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID))
+ stubFor(get(urlEqualTo("/v2/ans/agents/" + TEST_AGENT_ID))
.willReturn(aResponse()
.withStatus(418)
.withHeader("Content-Type", "application/json")
@@ -693,6 +697,7 @@ void shouldThrowWhenRegistrationMissingSelfLink(WireMockRuntimeInfo wmRuntimeInf
RegistrationClient client = RegistrationClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
+ .apiVersion(ApiVersion.V1)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
@@ -725,6 +730,7 @@ void shouldThrowWhenRegistrationHasNullLinks(WireMockRuntimeInfo wmRuntimeInfo)
RegistrationClient client = RegistrationClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
+ .apiVersion(ApiVersion.V1)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
@@ -743,11 +749,45 @@ void shouldThrowWhenRegistrationHasNullLinks(WireMockRuntimeInfo wmRuntimeInfo)
.hasMessageContaining("missing 'self' link");
}
+ @Test
+ @DisplayName("Should throw AnsServerException when v2 registration response has no agentId")
+ void shouldThrowWhenV2RegistrationMissingAgentId(WireMockRuntimeInfo wmRuntimeInfo) {
+ String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
+
+ // v2 resolves via agentId, not the self link; a response without it is a server error.
+ stubFor(post(urlEqualTo("/v2/ans/agents"))
+ .willReturn(aResponse()
+ .withStatus(202)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"status\":\"PENDING_VALIDATION\"}")));
+
+ RegistrationClient client = RegistrationClient.builder()
+ .environment(Environment.OTE)
+ .baseUrl(baseUrl)
+ .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
+ .build();
+
+ AgentRegistrationRequest request = new AgentRegistrationRequest()
+ .agentDisplayName("Test Agent")
+ .version("1.0.0")
+ .agentHost("test-agent.example.com")
+ .addEndpointsItem(new AgentEndpoint()
+ .protocol(Protocol.A2_A)
+ .agentUrl(URI.create("https://test-agent.example.com/a2a")))
+ .identityCsrPEM("test-csr")
+ .serverCsrPEM("test-csr");
+
+ assertThatThrownBy(() -> client.registerAgent(request))
+ .isInstanceOf(AnsServerException.class)
+ .hasMessageContaining("missing 'agentId'");
+ }
+
// ==================== Helper Methods ====================
private String registrationPendingResponse() {
return """
{
+ "agentId": "550e8400-e29b-41d4-a716-446655440000",
"status": "PENDING_VALIDATION",
"ansName": "ans://v1.0.0.test-agent.example.com",
"nextSteps": [
diff --git a/ans-sdk-spring-boot-starter/examples/spring-boot-app/src/main/java/com/godaddy/ans/examples/springboot/AgentController.java b/ans-sdk-spring-boot-starter/examples/spring-boot-app/src/main/java/com/godaddy/ans/examples/springboot/AgentController.java
index 762d092..9962e63 100644
--- a/ans-sdk-spring-boot-starter/examples/spring-boot-app/src/main/java/com/godaddy/ans/examples/springboot/AgentController.java
+++ b/ans-sdk-spring-boot-starter/examples/spring-boot-app/src/main/java/com/godaddy/ans/examples/springboot/AgentController.java
@@ -1,5 +1,6 @@
package com.godaddy.ans.examples.springboot;
+import com.godaddy.ans.sdk.config.Environment;
import com.godaddy.ans.sdk.discovery.DiscoveryClient;
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import com.godaddy.ans.sdk.model.generated.AgentRegistrationRequest;
@@ -68,9 +69,10 @@ public ResponseEntity resolve(
*/
@GetMapping("/health")
public ResponseEntity