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-credentialand binds it to one project plus already-approved account references. - The raw bootstrap credential is written once to a local
0600file. 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.