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 JWSTheregistrationfield in the body ofPOST /app/intent-requests/register. Every intent registration is signed with this JWT.Session JWTBearerSent asAuthorization: Bearer <token>to the Org API (/orgs/{org}/…). The developer console holds it after a passkey sign-in.poll_secret48 hex (header)Sent asX-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.
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_JWKThe Ed25519 private JWK as JSON. Only your server holds it.MASTER_PUBLIC_JWK_JSONThe master object from the aggregate JWKS, stored as a JSON string.CLAVIS_APP_IDThe app's id.audAlways app-intent-registration.issapp:{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/jsonThe 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.
const stored = await loadPollSecret(requestId);
const status = await app.getStatus(requestId, stored.pollSecret, stored.messageDigest);payload.poll_secretstring (48 hex)requiredFrom the register response. A new value for every request. Only your server keeps it.X-Intent-Poll-Secretrequest headerrequiredSent 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>"
}
}masterThe 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 /statusincludes it aspayload.sign_proof. - Governance requests to the Org API carry one in the body. There, purpose binds the operation.
shownis 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 noshown.
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.requestOpens a request with POST …/approval-requests.org.policy.voteOne vote with POST …/votes.org.approval.commitThe 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.