# Kvendra Wire Protocol — v1.0.0

> **Status**: Draft. M1 broker spec. Server-side implementation in progress (closed-source). The wire here is the canonical, cloud-agnostic contract — anyone can build a compatible server or client against it.

## Why this document exists

Kvendra is an [Open Core product](https://en.wikipedia.org/wiki/Open-core_model):

- **Client** (this repo, `kvendra-cli`): Apache 2.0, auditable, runs on the developer's machine. Holds the local vault, mints SDK-level abstractions, talks to MCP-aware agents.
- **Wire protocol** (this document): cloud-agnostic REST + JSON. No vendor lock-in. Versioned at the path (`/v1/...`).
- **Server implementations**: commercial, closed-source. SaaS-hosted at `kvendra.cloud`, self-hostable via the future helm chart. Anyone is welcome to build their own compatible server.

The contract below is the **only** surface a client should depend on. If the SaaS server adds vendor-specific fields, that is a server bug and a violation of the project's architectural decisions (`ADR-KVD-026`).

## Base URLs

| Host | Audience |
|---|---|
| `https://api.kvendra.com` | Foundation endpoints (`/v1/healthz`, `/v1/me`) and the developer-facing public API. |
| `https://api.kvendra.cloud` | Workspace, broker, audit, and token-vending (M1+, Team & Enterprise tiers). |

The split lives because of `ADR-KVD-027`. A self-hoster will choose their own hostnames; the paths and payloads stay the same.

## Authentication

Every endpoint except `/v1/healthz` requires:

```
Authorization: Bearer <jwt>
```

The JWT is OIDC-compliant, RS256-signed, and issued by the Kvendra OIDC provider (or by the equivalent IdP configured in a self-hosted deployment). Discovery:

```
GET https://auth.kvendra.cloud/.well-known/openid-configuration
```

The JWKS endpoint is published at the URL returned by discovery, and clients should cache keys for ~1 hour (rotations are coordinated with overlap windows; see `IF-KVD-ENTERPRISE-001`).

Custom claims use the `kvendra:` namespace — never any vendor-specific prefix.

For the public CLI flow, see [`kvendra login --workspace`](#client-login-flow) below.

## Versioning policy

The version lives in the path: `/v1/...`.

| Bump | Trigger | Examples |
|---|---|---|
| **MAJOR** | Breaking change. New version path; old version stays live for ≥6 months with `Deprecation` headers on every response. | Field removed, semantics changed, endpoint removed. |
| **MINOR** | Backwards-compatible additions. Same `/v1` path. | New endpoint, new optional field, new error type. |
| **PATCH** | Documentation clarification with no wire impact. | Wording, examples. |

Active version: **`v1.0.0`** (M1 release). See [CHANGELOG.md](./CHANGELOG.md).

## Endpoint summary

### Foundation

| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | `/v1/healthz` | none | Liveness probe. Returns `{ status, version }`. |
| GET | `/v1/me` | JWT | Authenticated user identity from the validated JWT. |

### Workspaces

| Method | Path | Required role | Purpose |
|---|---|---|---|
| GET | `/v1/workspaces` | any | List workspaces the caller is member of. |
| POST | `/v1/workspaces` | any | Create a new workspace under a tenant (creator becomes `owner`). |
| GET | `/v1/workspaces/{workspace_id}` | member | Workspace details + aggregate counters. |

### Members

| Method | Path | Required role | Purpose |
|---|---|---|---|
| GET | `/v1/workspaces/{workspace_id}/members` | member | List members. |
| POST | `/v1/workspaces/{workspace_id}/members` | admin / owner | Invite a member. |
| DELETE | `/v1/workspaces/{workspace_id}/members/{member_id}` | admin / owner | Revoke a member. |

### Profiles (custodied secrets)

| Method | Path | Required role | Purpose |
|---|---|---|---|
| GET | `/v1/workspaces/{workspace_id}/profiles` | member | List profiles (redacted — no secret material). |
| POST | `/v1/workspaces/{workspace_id}/profiles` | admin / owner | Add a profile. Plaintext travels TLS-only, persisted envelope-encrypted. |
| PUT | `/v1/workspaces/{workspace_id}/profiles/{profile_id}` | admin / owner | Rotate a profile's secret material. |
| DELETE | `/v1/workspaces/{workspace_id}/profiles/{profile_id}` | admin / owner | Revoke and remove a profile. |

### Policy templates

| Method | Path | Required role | Purpose |
|---|---|---|---|
| GET | `/v1/workspaces/{workspace_id}/templates` | member | List templates that scope profile capabilities. |
| POST | `/v1/workspaces/{workspace_id}/templates` | admin / owner | Create a template. |

### Token vending (core)

| Method | Path | Required role | Purpose |
|---|---|---|---|
| POST | `/v1/profiles/{profile_id}/tokens:issue` | member with permission for the (primitive, op) | Mint an ephemeral token for a primitive operation. The server validates the JWT, the workspace membership, the policy template, and the allowlist, then returns a short-lived credential plus the audit correlation identifier. |

### Audit & overview

| Method | Path | Required role | Purpose |
|---|---|---|---|
| GET | `/v1/workspaces/{workspace_id}/audit` | admin / owner | Paginated audit event query with cursor + filters. |
| GET | `/v1/workspaces/{workspace_id}/overview` | member | Aggregate counters (members, profiles, audit volume, expiring credentials). |

## Identifiers

- `tenant_id` — slug, lowercase + digits + hyphen: `^[a-z0-9-]+$` (`GLO-KVD-014`).
- `workspace_id` — `<tenant_slug>/<workspace_slug>`, both slugs matching the rule above (`GLO-KVD-013`).
- `member_id` — UUID v4.
- `profile_id` — slug + dots allowed: `^[a-z0-9._-]+$`.
- `audit_id` — [ULID](https://github.com/ulid/spec) (Crockford-base32, 26 chars).
- `args_hash` — SHA-256 over the canonicalized argument map (lowercase hex, 64 chars).

## Roles

| Role | Capabilities |
|---|---|
| `owner` | Full administrative + billing + workspace deletion. |
| `admin` | Full administrative minus billing. |
| `member` | Invoke authorized primitives. Read own audit. |
| `viewer` | Read-only dashboard. No invocations. |

## Quickstart

### 1. Discover identity

```bash
curl -fsSL https://auth.kvendra.cloud/.well-known/openid-configuration | jq .issuer
# "https://auth.kvendra.cloud"
```

### 2. Health probe (no auth)

```bash
curl -fsSL https://api.kvendra.com/v1/healthz
# {"status":"ok","version":"..."}
```

### 3. Authenticated identity

```bash
curl -fsSL https://api.kvendra.com/v1/me \
  -H "Authorization: Bearer $KVENDRA_JWT"
# {"user_id":"...","email":"...","plan":"free"}
```

### 4. Create a workspace

```bash
curl -fsSL https://api.kvendra.cloud/v1/workspaces \
  -H "Authorization: Bearer $KVENDRA_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "acme-corp",
    "workspace_slug": "frontend",
    "name": "Acme Frontend",
    "plan": "trial"
  }'
```

### 5. Mint an ephemeral token

```bash
ARGS_HASH=$(echo -n '{"repo":"acme-corp/frontend"}' | shasum -a 256 | awk '{print $1}')

curl -fsSL https://api.kvendra.cloud/v1/profiles/github.acme.deployer/tokens:issue \
  -H "Authorization: Bearer $KVENDRA_JWT" \
  -H "Content-Type: application/json" \
  -d "{
    \"primitive\": \"kvendra.git\",
    \"op\": \"clone\",
    \"args_hash\": \"$ARGS_HASH\",
    \"requested_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
  }"

# {
#   "token": "ghp_ephemeral_...",
#   "expires_at": "2026-05-11T16:10:00Z",
#   "audit_id": "01HXY8Z5K9M3Q4R7T8V0W1X2Y3",
#   "scope": { ... }
# }
```

The `audit_id` is your correlation key against the audit log — keep it in the local CLI's audit DB.

## Client login flow

The CLI uses standard OAuth 2.0 Authorization Code + PKCE:

1. `kvendra login --workspace acme-corp/frontend` generates a PKCE pair.
2. Opens the browser to `https://auth.kvendra.cloud/oauth2/authorize?...&code_challenge=...`.
3. The user authenticates (email/password or GitHub via federation).
4. The browser is redirected to `http://127.0.0.1:54321/callback` (local loopback server).
5. The CLI exchanges `code + code_verifier` for `id_token + refresh_token` via `POST /oauth2/token`.
6. Tokens are stored at `~/.kvendra/sessions/<workspace_id>.token` (mode `0600`).

The refresh token lives 30 days. The CLI auto-renews access tokens silently until either expires; on failure, it prompts a re-login.

## Errors

All errors share the same envelope:

```json
{
  "error": {
    "type": "membership_revoked",
    "message": "Your membership in workspace acme-corp/frontend was revoked."
  }
}
```

| Status | Common types |
|---|---|
| 400 | `invalid_request`, `invalid_secret_format` |
| 401 | `unauthorized`, `membership_revoked` |
| 403 | `insufficient_privilege`, `insufficient_permission`, `not_member` |
| 404 | `workspace_not_found`, `profile_not_found`, `member_not_found` |
| 409 | `workspace_exists`, `member_exists` |
| 410 | `profile_expired` |
| 429 | `rate_limited` (carries `retry_after_seconds`) |
| 5xx | `server_error`, `upstream_unavailable` |

## Cloud-agnostic guarantees

Per `ADR-KVD-026`, the wire protocol must not leak the server's implementation. Specifically:

- No URLs of cloud providers appear in response bodies.
- No vendor-specific identifiers (e.g. AWS ARNs, GCP resource IDs) are exposed.
- Pagination cursors are opaque base64url strings — clients must not parse them.
- JWT custom claims use the `kvendra:` namespace exclusively.

If a response ever leaks a vendor-specific string, that is a server-side bug. Please file an issue.

## Server implementations

| Implementation | Status | Operator |
|---|---|---|
| **SaaS Kvendra** at `*.kvendra.cloud` | live M1 (dogfooding) | Kvendra (closed-source, AGPL-13 boundary respected) |
| **Helm self-hosted chart** | planned M3 | Customer (Postgres + OIDC IdP + Vault / KMIP) |
| **Community-built** | possible | Anyone — the contract is in this document |

The SaaS and helm implementations share a TypeScript library (`@kvendra/enterprise-core`) so feature parity is guaranteed (`ADR-KVD-ENTERPRISE-003`). The wire protocol is identical.

## Reference machine-readable spec

A canonical OpenAPI 3.1 document backs this markdown. It lives in the closed-source server repo at `packages/protocol-spec/openapi.yaml`; the schemas, paths, parameters, and error shapes documented here are generated from that source. If the prose here drifts from the OpenAPI document, the OpenAPI document wins.

## References

- `ADR-KVD-026` — Open Core split: cliente Apache 2.0 + protocolo cloud-agnostic + servidor propietario.
- `ADR-KVD-ENTERPRISE-001` — Token-vending efímero por defecto.
- `ADR-KVD-ENTERPRISE-002` — Workspace + custodia centralizada.
- `IF-KVD-ENTERPRISE-001` — OIDC discovery + JWT shape + JWKS.
- `IF-KVD-ENTERPRISE-002` — Cloud broker REST API (this document's source of truth).
- `GLO-KVD-013` — `workspace_id` format.
- `GLO-KVD-014` — `tenant_id` format.
- `GLO-KVD-015` — Cloud broker definition.
- `GLO-KVD-016` — Secret resolver abstraction in the client.
- `SLA-KVD-ENTERPRISE-001` — Performance and availability targets for the SaaS deployment.
