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/jsonThe 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:
| Route | What it does |
|---|---|
GET /auth/login | Starts the OIDC flow |
GET /auth/callback | Completes it and sets the cp_session httpOnly cookie |
GET /auth/me | Returns the resolved principal (namespaces, role, admin) |
GET /auth/logout | Clears 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.
| Role | Can |
|---|---|
ROLE_VIEWER | Read within its namespaces: get/list jobs, sandboxes, environments, workspaces, egress events |
ROLE_OPERATOR | Everything a viewer can, plus mutate: run jobs, create sandboxes and workspaces, register/build environments, grant egress, read an app key |
ROLE_ADMIN | Cross-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
| Code | Meaning |
|---|---|
unauthenticated | Missing, malformed, expired or revoked credential |
permission_denied | Valid credential, wrong namespace or insufficient role |
unimplemented | The call is real but its backing capability isn't configured in this deployment (e.g. no preview domain, no build backend) |