Sockudo
Server

Authentication for AI Transport

Capability tokens, claims, channel patterns, refresh, revocation, and server-authorized requests.

AI Transport uses three layers of authority:

LayerUsed forAuthority
App-key HTTP authServer publishes, mutations, history reads, push management, revocationTrusted backend with app secret
Private/presence channel authSubscription admissionCustomer backend signs channel auth
Protocol V2 capability tokenClient connection identity and channel-scoped capabilitiesCustomer backend issues short-lived JWT

Never trust client_id from message bodies or AI headers. Verified identity comes from signed-in socket state, capability-token claims, or trusted server app-key context.

Capability token claims

Capability tokens are HS256 JWTs signed with the app secret. Header kid must equal the app key. Claims are:

{
  "x-sockudo-client-id": "user-42",
  "x-sockudo-capability": "{\"subscribe\":[\"private-ai:user-42:*\"],\"publish\":[\"private-ai:user-42:*\"],\"history\":[\"private-ai:user-42:*\"],\"message_append_own\":[\"private-ai:user-42:*\"]}",
  "iat": 1764835200,
  "nbf": 1764835200,
  "exp": 1764838800,
  "jti": "tok_01J..."
}

Limits from code: token at most 8 KiB, client_id at most 128 bytes, jti at most 128 bytes, lifetime at most 24 hours, and 30 seconds clock skew.

Node example

import crypto from "node:crypto";

function base64url(value) {
  return Buffer.from(JSON.stringify(value)).toString("base64url");
}

export function issueSockudoToken({ appKey, appSecret, clientId, capabilities, now = Math.floor(Date.now() / 1000) }) {
  const header = { alg: "HS256", typ: "JWT", kid: appKey };
  const payload = {
    "x-sockudo-client-id": clientId,
    "x-sockudo-capability": JSON.stringify(capabilities),
    iat: now,
    nbf: now,
    exp: now + 3600,
    jti: crypto.randomUUID()
  };
  const signingInput = `${base64url(header)}.${base64url(payload)}`;
  const signature = crypto.createHmac("sha256", appSecret).update(signingInput).digest("base64url");
  return `${signingInput}.${signature}`;
}

Rust example

use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use serde::Serialize;
use std::collections::BTreeMap;

#[derive(Serialize)]
struct Claims {
    #[serde(rename = "x-sockudo-client-id")]
    client_id: String,
    #[serde(rename = "x-sockudo-capability")]
    capability: String,
    iat: i64,
    nbf: i64,
    exp: i64,
    jti: String,
}

fn issue_sockudo_token(
    app_key: &str,
    app_secret: &str,
    client_id: &str,
    capabilities: BTreeMap<String, Vec<String>>,
    now: i64,
    jti: String,
) -> jsonwebtoken::errors::Result<String> {
    let mut header = Header::new(Algorithm::HS256);
    header.kid = Some(app_key.to_owned());
    encode(
        &header,
        &Claims {
            client_id: client_id.to_owned(),
            capability: serde_json::to_string(&capabilities).expect("capabilities serialize"),
            iat: now,
            nbf: now,
            exp: now + 3600,
            jti,
        },
        &EncodingKey::from_secret(app_secret.as_bytes()),
    )
}

Refresh and revocation

Refresh a V2 connection with sockudo:auth:

{
  "event": "sockudo:auth",
  "data": { "token": "<new-token>" }
}

Refresh cannot change client_id. To revoke tokens, call signed HTTP:

POST /apps/{appId}/revocations

with jti, client_id, or both. Matching local sockets receive token expiry/revocation handling and close with the configured grace path.

Capability patterns

Patterns accept exact channel names, *, and wildcard matching as implemented by ConnectionCapabilities. Keep user tokens narrow:

{
  "subscribe": ["private-ai:user-42:*"],
  "publish": ["private-ai:user-42:*"],
  "history": ["private-ai:user-42:*"],
  "presence": ["presence-ai:user-42:*"],
  "message_append_own": ["private-ai:user-42:*"],
  "message_update_own": ["private-ai:user-42:*"],
  "annotation-subscribe": ["private-ai:user-42:*"]
}

Agent/server workers should use trusted app-key HTTP or a deliberately scoped server-issued token.

On this page