Design Proposal: Concelier Credential Dry-Run Endpoint

Audience: Concelier/Excititor connector engineers and Platform reviewers weighing how to add a live vendor-credential probe. This is a proposal (a recommendation under review), not an accepted decision.

Status: Proposed Date: 2026-05-12 Sprint: docs/implplan/SPRINT_20260512_021_Concelier_Excititor_credentials_resolver_wiring.md — TASK-021-08 Upstream RFC: RFC-0001 — Connector Credentials Storage (§6.2 step 4) Authors: Concelier Connectors / Platform

Context

The Console Verify button (TASK-021-04, DONE) calls Platform’s POST /platform/api/v1/connector-credentials/{id}/verify. That endpoint runs a metadata-only path: it AEAD-decrypts the row using the per-tenant AAD, confirms the rotation state machine is sound, and returns { ok, reason, state }. It does not dial the upstream vendor API.

The follow-up Operators have asked for is a true probe — i.e. “given this stored credential, does the connector authenticate successfully against the upstream vendor today?” That requires Concelier (or Excititor) execution because Platform must remain vendor-agnostic.

Concelier already exposes POST /api/v1/advisory-sources/{sourceId}/check (SourceManagementEndpointExtensions.cs:372) which exercises connector connectivity but uses whatever overlay the connector currently resolves — there is no way to ask “exercise the connector with a specific credentialId instead of whatever the current overlay picks.”

Options

Option A — Extend /{sourceId}/check with ?credentialId=

POST /api/v1/advisory-sources/{sourceId}/check?credentialId={uuid} forces the connector to resolve the named credential row for the duration of the call and runs the existing connectivity probe.

Option B — New /{sourceId}/dry-run-credential verb

POST /api/v1/advisory-sources/{sourceId}/dry-run-credential with body { credentialId: "<uuid>" }.

Recommendation

Option B — new /{sourceId}/dry-run-credential verb.

The /check endpoint carries persistence semantics that are wrong for the Verify use case (a Verify click must never enable/disable a source). Bolting ?credentialId= + an implicit dryRun=true onto /check collapses two distinct user intents (operator says “is this source healthy enough to auto-toggle?” vs. operator says “does this specific credential authenticate?”) into one URL, and the audit trail (concelier.verify on advisory_source resource type) would conflate them. Explicit verb keeps the audit log readable and the scope-tightening option open.

Endpoint contract sketch

POST /api/v1/advisory-sources/{sourceId}/dry-run-credential
Authorization: Bearer <token with concelier:credentials:dry-run>
Content-Type: application/json

Request:
{
  "credentialId": "fbe1d6c4-...-...",      // required, UUID
  "timeoutSeconds": 15                      // optional, default 15, max 60
}

Response 200:
{
  "ok": true | false,
  "reason": null | "vendor_4xx" | "vendor_5xx" | "timeout" | "tls_handshake" | "scope_missing" | ...,
  "vendorStatus": 200,                      // HTTP status from vendor (when applicable)
  "elapsedMs": 842,
  "credentialState": "active" | "rotating",
  "probedAt": "2026-05-12T19:04:11Z"
}

Response 404: credentialId not found / not visible to tenant
Response 403: scope missing
Response 409: credential is revoked or pending (cannot dry-run)
Response 400: source does not support credentials / no probe defined

Hygiene rules (binding)

  1. Never returns plaintext — same posture as Platform /verify (RFC §7).
  2. No persistence side-effects — must not enable/disable the source, must not flip rotation state, must not increment last-success-at.
  3. Read-only audit rowconcelier.dryrun action on connector_credential resource type, with credentialId and sourceId in metadata. No plaintext, no AAD, no ciphertext bytes.
  4. Scope: new concelier:credentials:dry-run. Distinct from connector:credentials:read (which is a Platform-side read) and concelier.sources.manage (which is mutative).
  5. Rate limit: per-tenant 10 req/min — vendor probes are expensive and operators should not loop them.

Impact on existing /check

None. /{sourceId}/check keeps its current shape and semantics. ?credentialId= is not added. Operators continue to use /check for the auto-enable/disable workflow and the new /dry-run-credential verb for credential-specific probes.

Implementation surface

Open questions

Decision log