Developer console

Authentication

The Clavis API takes two kinds of credential: an operate-signed JWT for registering intents, and a session JWT Bearer for the organisation (Org).

Credentials

Intent registration and the Org Bearer.

  • Operate-signed JWTCompact JWS
    The registration field in the body of POST /app/intent-requests/register. Every intent registration is signed with this JWT.
  • Session JWTBearer
    Sent as Authorization: Bearer <token> to the Org API (/orgs/{org}/…). The developer console holds it after a passkey sign-in.
  • poll_secret48 hex (header)
    Sent as X-Intent-Poll-Secret. Only your server receives it, in the register response, and it is used for status.

Operate-signed JWT

createAppClient from @clavis.nexus/app builds and signs it. The operate private key is an Ed25519 private JWK.

TypeScript
import { createAppClient } from "@clavis.nexus/app";

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`missing env: ${name}`);
  return value;
}

const app = createAppClient({
  apiOrigin: "https://api.clavis.nexus",
  appId: requiredEnv("CLAVIS_APP_ID"),
  operatePrivateJwk: requiredEnv("OPERATE_PRIVATE_JWK"),
  masterPublicJwkJson: requiredEnv("MASTER_PUBLIC_JWK_JSON"),
});
  • OPERATE_PRIVATE_JWK
    The Ed25519 private JWK as JSON. Only your server holds it.
  • MASTER_PUBLIC_JWK_JSON
    The master object from the aggregate JWKS, stored as a JSON string.
  • CLAVIS_APP_ID
    The app's id.
  • aud
    Always app-intent-registration.
  • iss
    app:{app_id}, naming the app that issues it.

Session JWT

The Org Bearer after you sign in to the developer console.

  • After a passkey sign-in to the developer console, send the session JWT as a Bearer token.
  • Use it for operations by Org members, such as governance and app settings. The ledger is called with the app's operate key.

Example request

POST https://api.clavis.nexus/orgs/<org>/approval-requests
Authorization: Bearer <session JWT>
Content-Type: application/json

The governance API needs a sign_proof in the body as well as the session. The steps are in Approval rules.

poll_secret

getStatus adds the header to status calls and verifies the signature on the response.

TypeScript
const stored = await loadPollSecret(requestId);
const status = await app.getStatus(requestId, stored.pollSecret, stored.messageDigest);
  • payload.poll_secretstring (48 hex)required
    From the register response. A new value for every request. Only your server keeps it.
  • X-Intent-Poll-Secretrequest headerrequired
    Sent with status. Missing returns 401; a mismatch returns 403.

GET status (the HTTP the SDK sends)

GET https://api.clavis.nexus/app/intent-requests/status/<request_id>
X-Intent-Poll-Secret: <poll_secret>

Aggregate JWKS

The master public key you store in your environment, and verification of status response signatures. The public URL is GET https://api.clavis.nexus/.well-known/jwks.json.

Response (excerpt)

{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "kid": "<child-kid>",
      "alg": "ES256",
      "x": "<base64url>",
      "y": "<base64url>"
    }
  ],
  "master": {
    "kty": "EC",
    "crv": "P-256",
    "kid": "clavis-master",
    "alg": "ES256",
    "x": "<base64url>",
    "y": "<base64url>"
  }
}
  • master
    The instance master public key. Pass this object as a JSON string to masterPublicJwkJson in createAppClient.
  • keys[]
    Child public keys, one per use slot.

SignProof

Carried in status responses and in governance request bodies.

  • For a completed intent, GET /status includes it as payload.sign_proof.
  • Governance requests to the Org API carry one in the body. There, purpose binds the operation.
  • shown is the line Connect showed just before the signature. It is part of the challenge, so an edited SignProof fails verification. Signatures that do not go through Connect have no shown.

SignProof (shape, excerpt)

{
  "v": 1,
  "intent_type": "generic_sign",
  "intent_id": "<intent_id>",
  "message_digest": "<base64url>",
  "purpose": "generic.sign",
  "sub": "<passkey sub>",
  "rp_id": "<rp id>",
  "origin": "https://<host>",
  "challenge": "<base64url>",
  "binding_nonce": "<base64url>",
  "shown": { "app": "<app display name>", "host": "<app host>", "action": "<display_hint>" },
  "credential_id": "<base64url>",
  "counter_previous": 0,
  "authenticator_data": "<base64url>",
  "client_data_json": "<base64url>",
  "signature": "<base64url>",
  "public_key_cose_b64u": "<base64url>",
  "signed_at": 1710000000
}
  • org.approval.request
    Opens a request with POST …/approval-requests.
  • org.policy.vote
    One vote with POST …/votes.
  • org.approval.commit
    The commit with POST …/commit, once, after the threshold is reached.

Each governance mutation carries a SignProof for its purpose in the body. The flow is in Approval rules.

Reference

  • Quickstart — Setting up the Org, the app and the host check.
  • Signatures — The HTTP for register and status.
  • Approval rules — approval-requests, votes and the commit.
  • Ledger — Appending with the app's operate key.