Skip to content

Bridge enrollment HTTP contract

Bridge enrollment is a separate HTTP boundary from MCP OAuth. It establishes one project-fenced Bridge identity and returns a short-lived opaque token for Bridge transport. It does not create a provider account, choose a model, or grant access to another project.

Ownership and credentials

  • The server owns the project binding, Bridge identity, approved account references, challenge consumption, token hash and revocation state.
  • An operator creates a one-time bootstrap credential with heyaira-admin create-bridge-enrollment-credential and binds it to one project plus already-approved account references.
  • The raw bootstrap credential is written once to a local 0600 file. The database stores only its SHA-256 hash.
  • The Bridge sends its public Ed25519 key fingerprint in the challenge request and proves possession by signing the returned nonce.
  • Private keys, raw nonces, signatures, provider credentials and raw Bridge access tokens are never persisted or written to task state, receipts or logs.

The service must have two independent high-entropy configuration values: HEYAIRA_BRIDGE_ENROLLMENT_SECRET and HEYAIRA_BRIDGE_TOKEN_SECRET. They are not MCP OAuth secrets and are never put in a request body or response. HEYAIRA_BRIDGE_ROTATION_TTL_SECONDS bounds the prepare/confirm window and defaults to 300 seconds.

1. Create a challenge

POST /bridge/v1/enrollment/challenges
Authorization: Bearer <one-time-bootstrap-credential>
Idempotency-Key: bridge-enroll-<unique-key>
Content-Type: application/json
{
  "project_id": "<project-uuid>",
  "bridge_id": "mac-primary",
  "device_id": "<device-identifier>",
  "display_name": "Developer Mac",
  "public_key_fingerprint": "sha256:<64-lowercase-hex-chars>"
}

Successful response (201) contains no bootstrap token and no private data:

{
  "challenge_id": "<challenge-uuid>",
  "project_id": "<project-uuid>",
  "bridge_id": "mac-primary",
  "public_key_fingerprint": "sha256:<64-lowercase-hex-chars>",
  "nonce_b64": "<url-safe-32-byte-nonce>",
  "expires_at": "2026-09-21T12:35:00+00:00"
}

The approved account references are copied from the bootstrap credential by the server. The Bridge cannot add or replace them in this request.

2. Prove possession and activate

POST /bridge/v1/enrollment/proofs
Idempotency-Key: bridge-proof-<unique-key>
Content-Type: application/json
{
  "project_id": "<project-uuid>",
  "bridge_id": "mac-primary",
  "challenge_id": "<challenge-uuid>",
  "nonce_b64": "<nonce-from-challenge>",
  "signature_b64": "<ed25519-signature-over-nonce>",
  "public_key_b64": "<ed25519-public-key>"
}

Successful response (200) contains the new Bridge-scoped bearer token. The token is returned only from this response and is stored by the Bridge in its local secure store:

{
  "project_id": "<project-uuid>",
  "bridge_id": "mac-primary",
  "access_token": "<opaque-bearer-token>",
  "token_type": "Bearer",
  "scope": "bridge:transport",
  "expires_at": "2026-09-21T12:50:00+00:00"
}

Challenge consumption, identity activation and token-hash insertion commit in one PostgreSQL transaction. A failed proof leaves the identity pending and the challenge unconsumed. Reusing a successful proof with a new idempotency key is rejected; retrying the exact successful request with the same key returns the original response and does not mint another token.

3. Rotate a Bridge token

Rotation uses an explicit prepare/confirm protocol. A pending successor is not an active Bridge credential and cannot authorize normal Bridge work.

POST /bridge/v1/tokens/rotate/prepare
Authorization: Bearer <bridge-access-token>
Idempotency-Key: bridge-rotate-<unique-key>

Prepare returns rotation_id, a pending successor token and confirmation_expires_at. The predecessor remains active. An exact retry with the same predecessor and idempotency key returns the same pending successor, so a lost response does not lock out the Bridge. A different prepare is rejected while one successor is pending.

The Bridge stores both credentials and its rotation journal durably before it confirms:

POST /bridge/v1/tokens/rotate/confirm
Authorization: Bearer <pending-successor-token>
Idempotency-Key: bridge-confirm-<unique-key>
Content-Type: application/json

{"rotation_id":"<rotation-uuid>"}

Confirm atomically activates the successor, assigns its normal access-token expiry and revokes the predecessor. Retrying the exact successful confirmation with the successor returns the same non-secret status response. A revoked predecessor can never recover its successor.

The Bridge may explicitly abandon a pending successor with the same request shape at POST /bridge/v1/tokens/rotate/abort. Expiry or abort revokes only the pending successor and leaves the predecessor active. Emergency revocation is separate from routine rotation and also cancels every pending successor for the Bridge identity.

The retired one-phase POST /bridge/v1/tokens/rotate route fails closed with bridge_rotation_protocol_upgrade_required; it never issues or revokes a token. See the normative Bridge protocol.

4. Revoke a Bridge token

POST /bridge/v1/tokens/revoke
Authorization: Bearer <bridge-access-token>

Revocation is terminal for that token. A revoked Bridge identity also blocks the use or rotation of its tokens through the identity status check.

Stable errors

All enrollment errors use this shape and include Cache-Control: no-store:

{
  "error": {
    "code": "versioned_machine_code",
    "message": "short human-readable explanation"
  }
}

The contract uses these codes: bridge_enrollment_credential_required, bridge_enrollment_credential_invalid, bridge_enrollment_credential_expired, bridge_enrollment_credential_used, bridge_enrollment_credential_revoked, project_mismatch, enrollment_challenge_not_found, enrollment_challenge_mismatch, enrollment_challenge_expired, enrollment_challenge_consumed, enrollment_proof_invalid, enrollment_key_mismatch, bridge_not_pending, bridge_identity_conflict, bridge_persistence_error, bridge_revoked, bridge_token_invalid, bridge_token_expired, bridge_token_revoked, bridge_token_pending, bridge_rotation_protocol_upgrade_required, bridge_rotation_in_progress, bridge_rotation_not_found, bridge_rotation_expired, bridge_rotation_aborted, bridge_rotation_confirmed, bridge_rotation_invalid_state, bridge_rotation_predecessor_revoked, bridge_rotation_predecessor_expired, idempotency_key_required, and idempotency_conflict.

Rollout boundary

This contract is implemented locally in phase 1. It uses migrations 012, 013 and 014 plus BridgePersistenceAdapter. It does not implement offers, OpenBao credential leases, session registry, task dispatch or production activation. Those remain separate reviewed phases.