Xeonr Developer Docs

Authentication

Machine API keys, human OIDC sign-in, and how roles are resolved per namespace.

Every call is authenticated by one connect interceptor that resolves the caller to a principal — a set of namespaces, a role in each, and whether it holds admin. Handlers enforce against that; there is no privileged side channel, and the console is a plain client of the same API.

Machine callers — API keys

An API key is issued for exactly one namespace and one role. It is a bearer token prefixed cpk_:

POST /containers.api.v1.JobService/RunJob HTTP/1.1
Host: containers.xeonr.io
Authorization: Bearer cpk_...
Content-Type: application/json

The namespace is pinned server-side, so a key literally cannot name another namespace: a request for someone else's job or workspace fails with permission_denied, not an empty result.

const { key, plaintext } = await cp.raw.namespace.issueApiKey({
  namespace: "my-app",
  name: "ci",
  role: Role.OPERATOR,
});

NamespaceService is admin-only and reached through cp.raw; the SDK models the surface a namespace-scoped caller uses.

plaintext is returned once, on issue, and is never readable again — store it immediately. ListApiKeys shows key_id, name, role, created and last-used; RevokeApiKey kills one by id.

Human callers — Xeonr Auth OIDC

The console signs in through Xeonr Auth with authorization code + PKCE. The api hosts the flow itself, so the browser never handles a token in JavaScript:

RouteWhat it does
GET /auth/loginStarts the OIDC flow
GET /auth/callbackCompletes it and sets the cp_session httpOnly cookie
GET /auth/meReturns the resolved principal (namespaces, role, admin)
GET /auth/logoutClears the session

The resolver accepts either an Authorization: Bearer header or the cp_session cookie, so a human token works against the API directly if you have one.

Roles

There is no platform-side membership table for humans. A namespace declares OIDC group → role bindings; the interceptor reads the caller's groups claim and maps it through every namespace's bindings.

RoleCan
ROLE_VIEWERRead within its namespaces: get/list jobs, sandboxes, environments, workspaces, egress events
ROLE_OPERATOREverything a viewer can, plus mutate: run jobs, create sandboxes and workspaces, register/build environments, grant egress, read an app key
ROLE_ADMINCross-namespace. Reachable via the admin OIDC scope, the platform-admin group, or an ADMIN-role API key

Admin is unconditional rather than enumerated: an admin is not a member of every namespace, it simply passes every namespace check. NamespaceService — creating namespaces, editing group bindings and quotas, registering internal egress targets, issuing and revoking keys — is admin-only in full.

await cp.raw.namespace.updateNamespace({
  namespace: "my-app",
  groups: [
    { oidcGroup: "my-team", role: Role.OPERATOR },
    { oidcGroup: "support", role: Role.VIEWER },
  ],
});

Group bindings are a full replace, as are internal_targets and image_pull_secrets. Send the complete list.

Scoped keys inside a sandbox

A sandbox's agent holds its own key rather than a namespace-wide one. Where a workspace is attached, that key carries a workspace scope: WorkspaceService will only serve operations on that one workspace id, so a compromised sidecar can't read its neighbours in the same namespace. This is issued and managed by the platform — nothing to configure.

Errors

CodeMeaning
unauthenticatedMissing, malformed, expired or revoked credential
permission_deniedValid credential, wrong namespace or insufficient role
unimplementedThe call is real but its backing capability isn't configured in this deployment (e.g. no preview domain, no build backend)

On this page