Design: Auth API
Prerequisites
- trellis-auth.md - auth architecture and identity authority model
- auth-protocol.md - proofs, connect tokens, and internal state rules
Scope
This document defines the public Trellis auth API.
It covers:
- browser-flow broker, OAuth, and bind endpoints
- browser-flow APIs consumed by portals, including detached CLI/native login
- HTTP device activation endpoints
- public and admin
rpc.Auth.*endpoints - emitted auth events
It does not define language-specific client APIs.
Headings in this document use logical grouped resource-first names such as rpc.Auth.Devices.List. The wire subjects remain versioned forms such as rpc.v1.Auth.Devices.List and operations.v1.Auth.DeviceUserAuthorities.Resolve.
Public names use the resource group before the action. Examples:
Auth.Deployments.CreateAuth.Devices.ListAuth.DeploymentAuthority.ListAuth.DeploymentAuthority.GetAuth.DeploymentAuthority.Plans.ListAuth.DeploymentAuthority.Plans.GetAuth.DeploymentAuthority.PlanAuth.DeploymentAuthority.AcceptUpdateAuth.DeploymentAuthority.AcceptMigrationAuth.DeploymentAuthority.RejectAuth.DeploymentAuthority.Reconcile
Shared browser-consent capability views use this shape:
type ContractConsentCapability = {
displayName: string;
description: string;
consequence?: string;
}; HTTP Endpoints
Browser auth endpoints:
POST /auth/requestsGET /auth/login/:providerGET /auth/callback/:providerGET /auth/flow/:flowIdPOST /auth/flow/:flowId/register/localPOST /auth/flow/:flowId/approvalPOST /auth/flow/:flowId/bind
Global CORS behavior derives from web.origins:
web.origins: ["*"]allows arbitrary browser origins without credentials. This is appropriate for public Trellis APIs where requests carry explicit Trellis proofs rather than ambient cookies.- Specific
web.originsentries allow only those configured origins and enable credentialed CORS for those origins. - Flow-specific routes still perform stricter flow-local validation. Portal
endpoints allow the selected portal origin for the flow. Bind allows only the
app origin recorded when the flow was created from
redirectTo. Credentialed routes such as OAuth callback state handling keep their stricter CORS/cookie rules.
Public Trellis URLs and public NATS/WebSocket transports should use HTTPS/WSS.
Loopback HTTP/WS remains valid for local development, and explicit web.allowInsecureOrigins entries may permit other insecure public origins for
controlled deployments. Internal HTTP behind a trusted HTTPS reverse proxy is
still valid when the public browser-facing URL is secure.
Activated-device endpoints are defined in device-activation.md:
POST /auth/devices/activate/requestsPOST /auth/devices/activate/waitPOST /auth/devices/connect-info
POST /auth/devices/activate/requests validates the outbound device activation
payload, creates a short-lived auth-owned browser flow with kind: "device_activation", resolves the activation portal, and returns a short flowId-based activationUrl. Portal resolution comes from the preregistered
device instance and deployment-owned portal-route metadata, with fallback to the
built-in Trellis device portal. Callers do not provide portal ids or profile ids
in the normal path.
POST /auth/requests
Starts the normal auth flow for a contract-bearing user client such as a browser
app, CLI, or native app. The caller sends the initiating contract in the request
body so auth can either auto-complete reauth immediately or create an auth-owned
browser flow and return a short flowId-based login URL.
Request body:
| Name | Required | Description |
|---|---|---|
provider | no | Preferred provider id for direct provider continuation |
redirectTo | yes | Post-login redirect URL |
sessionKey | yes | Client public session key |
sig | yes | sign(hash("oauth-init:" + redirectTo + ":" + (provider ?? "") + ":" + canonicalJson(contract) + ":" + canonicalJson(context ?? null))) by sessionKey |
contract | yes | Initiating user-client contract manifest JSON for portal routing and consent planning |
context | no | Opaque JSON payload for app and portal coordination |
Behavior:
- Validate
redirectTo - Verify
sigbysessionKey - Validate the initiating contract and compute its digest
- If an existing delegated user session for that
sessionKeyalready covers the requested contract access, rebind immediately and returnstatus: "bound" - Otherwise create an auth-owned browser flow record
- Resolve login portal routing from auth-owned portal route selectors using app contract id and origin
- Otherwise use the DB-projected built-in Trellis login portal served by the Trellis HTTP server
- Return
status: "flow_started"with{ flowId, loginUrl }
Rules:
- user-facing apps and tools send their contract manifest when they initiate login; they receive per-user consent during auth rather than being pre-installed like services
- app, CLI, and native auth may present a contract digest first; when auth does
not know that digest it returns
manifest_required, and the client retries with the full manifest for validation, digest verification, and flow storage - bind later uses the contract already stored on the auth-owned browser flow rather than requiring the browser app to resubmit it
- if present,
contextis stored on the browser flow and returned to portals as app-owned opaque data - a portal is trusted for this redirect only because auth-owned login portal routing selected it for the flow; portal routes do not by themselves grant delegated consent or service authority
- first login does not require pre-registering a portal because the built-in Trellis login portal is always available
- auth MAY apply a matching grant override for the app’s contract id and origin;
when one matches, or when existing identity authority already grants a strict
superset of the requested access for the same app identity, auth may skip
browser UX and return
bounddirectly
GET /auth/login/:provider
Initiates authentication for a configured provider for an existing browser flow, usually after portal has chosen a provider.
Query parameters:
| Name | Required | Description |
|---|---|---|
flowId | yes | Browser flow id created by POST /auth/requests |
Behavior:
- Load the browser flow
- Generate OAuth state and PKCE challenge
- Store
{ provider, flowId, codeVerifier, createdAt } - Set
trellis_oauth=state - Redirect to the provider
Rules:
- the OAuth state cookie is
Securefor HTTPS public origins - loopback HTTP origins remain allowed without extra configuration for local development
- non-loopback HTTP public origins MUST be explicitly allowlisted with
web.allowInsecureOrigins
GET /auth/callback/:provider
Handles provider callback and returns control to portal for the next browser-flow step.
Behavior:
- Verify cookie matches
state - Lookup and CAS-delete the pending OAuth state
- Exchange code for tokens
- Fetch user info
- Provision or refresh the auth-local user projection
- Generate
authToken - Update the browser flow and pending auth state
- Delete cookie
- Redirect back into portal with
flowIdso portal can reload browser-flow state and follow the next server-generated redirect when appropriate
Rules:
- callback redirects preserve
flowId; they do not need to carrytrellisUrlin the default model because the selected portal deployment already has an explicit Trellis instance URL configuration - if an OAuth/OIDC callback resolves to an unknown federated identity, Trellis may self-register it only when the selected login portal’s effective policy allows federated registration and the provider is configured for the instance
- the selected login portal may further restrict federated providers;
nullmeans all configured providers are allowed,[]means none are allowed, and a non-empty list means only those configured provider ids may continue
GET /auth/flow/:flowId
Returns machine-readable browser flow state for portal.
Response model:
type PortalFlowState =
| {
status: "choose_provider";
flowId: string;
// Effective providers after the selected portal policy is applied.
providers: Array<{
id: string;
displayName: string;
}>;
app: {
contractId: string;
contractDigest: string;
displayName: string;
description: string;
origin?: string;
context?: unknown;
};
portal?: {
portalId: string;
displayName: string;
entryUrl: string | null;
builtIn: boolean;
disabled: boolean;
createdAt: string;
updatedAt: string;
};
registration?: {
localIdentity: { available: boolean };
federatedIdentity: {
available: boolean;
providers: Array<{ id: string; displayName: string }>;
};
};
}
| {
status: "approval_required";
flowId: string;
user: {
origin: string;
id: string;
name?: string;
email?: string;
image?: string;
};
approval: {
contractId: string;
contractDigest: string;
displayName: string;
description: string;
capabilities: Record<string, ContractConsentCapability>;
};
}
| {
status: "approval_denied";
flowId: string;
returnLocation?: string;
approval: {
contractId: string;
contractDigest: string;
displayName: string;
description: string;
capabilities: Record<string, ContractConsentCapability>;
};
}
| {
status: "insufficient_capabilities";
flowId: string;
approval: {
contractId: string;
contractDigest: string;
displayName: string;
description: string;
capabilities: Record<string, ContractConsentCapability>;
};
missingCapabilities: string[];
userCapabilities: string[];
}
| {
status: "redirect";
location: string;
}
| {
status: "expired";
}; Rules:
- portal renders UX only from this auth-owned flow state
- custom portal libraries should use
flowIdas their browser URL state and call this endpoint to render auth state; they should not depend on provider-specific fragments or portal-local query conventions as protocol authority - portal MUST treat
redirect.locationas an opaque next auth step redirect.locationmay point back to the originating browser app or to another auth-owned step in the same login flowapproval_deniedis a fallback state for stored denied flow state; normal user denial returnsredirectto the originating app withauthError=approval_denied, and portal helpers MAY treat anapproval_denied.returnLocationas an immediate redirect target- for detached CLI/native login,
redirect.locationmay resolve to the same portal login page; the built-in Trellis portal treats that same-page redirect as completion UX and tells the user to return to the Trellis CLI rather than redirecting again - portal does not invent auth-protocol next-step URLs locally, though it may still use its own local routes and UI state while rendering the flow
- portal-specific customization data travels through
app.contextrather than ad hoc query parameters between app and portal - portal registration UI is gated by auth-owned flow state; clients MUST use
registration.localIdentityandregistration.federatedIdentityrather than inferring registration availability from provider lists or local UI defaults - browser-visible
flowIdvalues are ULIDs; they are identifiers, not bearer secrets - framework-neutral browser helpers and thin framework wrappers may hide the
fetch and redirect plumbing, but exact helper declarations belong in the
generated
/apireference rather than in design docs
POST /auth/flow/:flowId/register/local
Registers a local username/password identity for the selected browser login flow and returns the next browser-flow state.
Local password credentials use Argon2id. The default minimum length is 12 characters; deployments may lower it only to the hard floor of 8. Trellis does not impose composition rules, and the current implementation does not maintain a PBKDF2 compatibility path for old hashes.
Request body:
{
username: string;
password: string;
name: string;
email: string;
} Rules:
- local self-registration is allowed only when the selected login portal’s
effective policy enables local registration and the instance-level
auth.localIdentity.enabledgate is enabled - the request body uses
nameandemail; portals MUST NOT split this intofirstNameorfamilyNamewire fields - successful local registration creates the account, local identity, password credential, and pending browser auth state atomically for the active flow
- duplicate local usernames and unavailable local registration are expected caller-visible failures, not unexpected server errors
- duplicate local usernames return
409witherror: "username_taken"; email uniqueness is not enforced by this endpoint and portals MUST NOT infer anemail_takenerror
POST /auth/flow/:flowId/approval
Accepts the portal consent decision for the contract attached to the browser
flow and returns the next PortalFlowState. The endpoint path is retained as a
public browser-flow URL shape.
Rules:
- the portal is not trusted as a service when it submits a consent decision
- auth trusts only the active browser flow identified by
flowIdand the server-owned state attached to that flow - public portal helpers may expose decisions as
"approved" | "denied", but the HTTP request body remains the canonical boolean shape below approved: truepersists the identity grant when no existing account-scoped identity authority or grant override already covers the request, then returns the normal redirect/bind continuation- persisted consent reuse is scoped to the Trellis user account and app identity anchor; the current provider origin/subject is retained as audit evidence and is not the matching key
approved: falsedoes not persist a denied contract decision; it consumes the pending browser flow and returns a redirect to the caller’sredirectTowithauthError=approval_denied- callers that receive
authError=approval_deniedSHOULD surface a denial result and clean the callback query parameters rather than immediately starting another sign-in flow
Request:
{
approved: boolean;
} POST /auth/flow/:flowId/bind
Binds a session key to an authenticated identity and approved contract digest for the normal browser flow path.
Request:
{
sessionKey: string;
sig: string; // sign(hash("bind-flow:" + flowId))
} Response:
type BindResponse =
| {
status: "bound";
inboxPrefix: string;
expires: string;
sentinel: {
jwt: string;
seed: string;
};
transports: {
native?: {
natsServers: string[];
};
websocket?: {
natsServers: string[];
};
};
}
| {
status: "insufficient_capabilities";
approval: {
contractDigest: string;
contractId: string;
displayName: string;
description: string;
capabilities: Record<string, ContractConsentCapability>;
};
missingCapabilities: string[];
userCapabilities: string[];
}; Behavior:
- Load the browser flow by
flowId - Load the pending authenticated state already attached to that flow
- Verify
sessionKeyandsig - Read the contract already associated with the pending login
- Validate the contract, compute digest, derive required capabilities, and check identity authority
- Reject the bind if the user projection is inactive
- Consume the pending auth state
- Create or recover the session record keyed by
sessionKey - Persist delegated contract metadata and delegated publish/subscribe subjects into the session
- Compute
inboxPrefix = _INBOX.${sessionKey.slice(0, 16)} - Refresh the Trellis-local auth projection entry without overwriting
admin-managed
activestate or granted capabilities - Return the bind response with
inboxPrefix,expires,sentinel, andtransports
Rules:
- normal browser and detached CLI/native flows bind only through the auth-owned browser flow after Trellis has already recorded a consent decision
- flow bind still rechecks identity authority and capabilities defensively
- portal is a browser UX surface only; bind remains auth-owned
Identity Authority RPCs
rpc.Auth.IdentityGrants.List
Request:
{
user?: string;
offset?: number;
limit: number;
} Response:
type AuthIdentityGrantRow = {
user: string;
answer: "approved" | "denied";
answeredAt: string;
updatedAt: string;
identityGrantId: string;
identityAnchor:
| { kind: "web"; contractId: string; origin: string }
| { kind: "cli"; contractId: string; sessionPublicKey: string }
| { kind: "native"; contractId: string; sessionPublicKey: string }
| { kind: "device-user"; contractId: string; devicePublicKey: string };
presentedContract: {
contractDigest: string;
contractId: string;
};
displayName: string;
description: string;
capabilities: Record<string, ContractConsentCapability>;
participantKind: "app" | "agent";
};
{
entries: AuthIdentityGrantRow[];
count: number;
offset: number;
limit: number;
nextOffset?: number;
} Callers without admin see only their own identity grants.
Identity grants are account-scoped: linked local and OIDC identities on the same
Trellis user account see and reuse the same grants for the same app identity
anchor. contractDigest and the provider identity that created the grant are
evidence metadata, not reuse keys.
List RPCs use the standard live offset page shape. Requests are { offset?: number; limit: number } plus documented filters. Responses are { entries, count, offset, limit, nextOffset? }. This is live offset
pagination, not snapshot or cursor pagination: concurrent inserts or deletes can
change which rows appear at later offsets. limit is required, offset is
optional and defaults to the first row, and implementations MUST apply any
filters in the database query before applying the bound.
rpc.Auth.IdentityGrants.Revoke
Request:
{
identityGrantId: string;
user?: string;
} Response:
{
success: boolean;
} Revocation removes the addressed identity grant, revokes matching active
delegated sessions, and removes reconnect authority until a new consent decision
creates an identity grant again. contractDigest is evidence metadata, not the
revocation key.
Authenticated User RPCs
These RPCs require session-key and proof headers. The contract digest is
authenticated during connect, bootstrap, or session binding and is resolved for
each request from stored session/principal state rather than from a per-request
header.
The following self-service auth RPCs intentionally require no granted capabilities beyond successful authenticated user context:
rpc.Auth.Sessions.Merpc.Auth.Sessions.Logoutrpc.Auth.Users.IdentityLink.Createrpc.Auth.Users.Password.Change
rpc.Auth.Sessions.Logout
Request:
{} Response:
{
success: boolean;
} Behavior:
- Validate headers
- Lookup session
- List connections for the session
- Delete the session
- Kick all connections
- Delete connection entries
Digest changes are handled by restarting the normal auth request flow with the
current contract body. Runtime reconnect auth is regenerated locally from sessionKey + contractDigest + iat + sig; auth does not issue renewable binding
tokens.
rpc.Auth.Sessions.Me
Request:
{} Response:
{
participantKind: "app" | "agent" | "service" | "device";
user: {
userId: string;
active: boolean;
email: string;
name: string;
image?: string;
capabilities: string[];
identity: {
identityId: string;
provider: string;
subject: string;
};
lastLogin?: string;
} | null;
device: {
type: "device";
deviceId: string;
deviceType: string;
runtimePublicKey: string;
deploymentId: string;
capabilities: string[];
active: boolean;
} | null;
service: {
type: "service";
id: string;
name: string;
capabilities: string[];
active: boolean;
} | null;
} Rules:
- this is a zero-capability authenticated self-service RPC
- user sessions receive user identity authority context and null device/service entries
- device sessions receive device materialized authority context and, when
available, the activating user in
user - service sessions receive service materialized authority context and null user/device entries
rpc.Auth.Requests.Validate
Request:
{
sessionKey: string;
proof: string;
subject: string;
payloadHash: string;
iat: number;
requestId: string;
capabilities?: string[];
} Response:
type CallerView =
| {
type: "user";
participantKind: "app" | "agent";
userId: string;
identity: {
identityId: string;
provider: string;
subject: string;
};
email: string;
name: string;
image?: string;
capabilities: string[];
active: boolean;
}
| {
type: "service";
id: string;
name: string;
capabilities: string[];
active: boolean;
}
| {
type: "device";
deviceId: string;
deviceType: string;
runtimePublicKey: string;
deploymentId: string;
capabilities: string[];
active: boolean;
};
{
allowed: boolean;
inboxPrefix: string;
caller: CallerView;
} This RPC is the capability and session lookup service used by other Trellis services. The caller shape is a union because users and devices all share the same post-auth authorization pipeline.
The proof covers the session key, request subject, raw payload hash, corrected iat, and requestId. Empty proof, session, subject, payload-hash, request-id,
or capability strings are invalid. payloadHash is the hash of the raw request
body computed by the receiving service; callers do not get to override it with a
trusted header. Validation uses a replay cache keyed by session and request id.
The proof does not include contractDigest; validation resolves contract
context, principal identity, and capabilities from the authenticated session
state that was created at connect, bootstrap, or session binding time.
Auth.Requests.Validate is a baseline auth surface for service runtimes.
Trellis may make it available to services automatically, without requiring every
service contract to declare an explicit auth uses entry for this RPC.
Device Activation And Deployment Authority Public Surface
Detailed activation flow semantics, event ordering, and confirmation-code behavior are defined in device-activation.md. This section defines the canonical public API shapes that other auth docs refer to.
Public auth-owned surfaces:
- HTTP endpoints
POST /auth/devices/activate/requests,POST /auth/devices/activate/wait, andPOST /auth/devices/connect-info - operation subject
operations.v1.Auth.DeviceUserAuthorities.Resolve - grouped deployment, deployment-authority, service-instance, device-instance,
and device lifecycle admin RPCs under
rpc.v1.Auth.* - event subjects
events.v1.Auth.DeviceUserAuthorities.Requested,events.v1.Auth.DeviceUserAuthorities.Approved,events.v1.Auth.DeviceUserAuthorities.Resolved, andevents.v1.Auth.DeviceUserAuthorities.ReviewRequested
Shared request and response types:
type ActivationDecisionReason = string; // deployment-defined machine-readable code
type DeploymentAuthoritySurfaceKind = "rpc" | "operation" | "event" | "feed";
type DeploymentAuthoritySurfaceAction =
| "call"
| "publish"
| "subscribe"
| "observe"
| "cancel";
type DeploymentAuthorityResourceKind =
| "kv"
| "store"
| "jobs"
| "event-consumer"
| "transfer";
type DeploymentAuthorityContractNeed = {
contractId: string;
required: boolean;
};
type DeploymentAuthoritySurfaceNeed = {
contractId: string;
kind: DeploymentAuthoritySurfaceKind;
name: string;
action?: DeploymentAuthoritySurfaceAction;
required: boolean;
};
type DeploymentAuthorityCapabilityNeed = {
capability: string;
required: boolean;
};
type DeploymentAuthorityResourceNeed = {
kind: DeploymentAuthorityResourceKind;
alias: string;
required: boolean;
definition?: Record<string, unknown>;
};
type DeploymentAuthorityNeeds = {
contracts: DeploymentAuthorityContractNeed[];
surfaces: DeploymentAuthoritySurfaceNeed[];
capabilities: DeploymentAuthorityCapabilityNeed[];
resources: DeploymentAuthorityResourceNeed[];
};
type DeploymentAuthoritySurface = {
contractId: string;
kind: DeploymentAuthoritySurfaceKind;
name: string;
action?: DeploymentAuthoritySurfaceAction;
};
type DeploymentAuthorityResource = {
kind: DeploymentAuthorityResourceKind;
alias: string;
required: boolean;
definition?: Record<string, unknown>;
};
type ContractProposal = {
deploymentId: string;
contractId: string;
contractDigest: string;
requestedNeeds: DeploymentAuthorityNeeds;
providedSurfaces: DeploymentAuthoritySurface[];
summary?: Record<string, unknown>;
};
type DeploymentAuthority = {
deploymentId: string;
kind: "service" | "device" | "app" | "cli" | "native" | "device-user";
disabled: boolean;
desiredState: {
needs: DeploymentAuthorityNeeds;
capabilities: string[];
resources: DeploymentAuthorityResource[];
surfaces: DeploymentAuthoritySurface[];
};
version: string;
createdAt: string;
updatedAt: string;
};
type MaterializedAuthorityCapabilityGrant = {
capability: string;
};
type MaterializedAuthoritySurfaceGrant = {
contractId: string;
surfaceKind: DeploymentAuthoritySurfaceKind;
name: string;
action?: DeploymentAuthoritySurfaceAction;
};
type MaterializedAuthorityNatsGrant = {
direction: "publish" | "subscribe";
subject: string;
surface?: {
contractId: string;
kind: DeploymentAuthoritySurfaceKind;
name: string;
action?: DeploymentAuthoritySurfaceAction;
};
requiredCapabilities: string[];
grantSource:
| "owned-surface"
| "used-surface"
| "resource-binding"
| "platform-service"
| "transfer";
};
type MaterializedAuthorityGrants = {
capabilities: MaterializedAuthorityCapabilityGrant[];
surfaces: MaterializedAuthoritySurfaceGrant[];
nats: MaterializedAuthorityNatsGrant[];
};
type MaterializedAuthority = {
deploymentId: string;
desiredVersion: string;
status: "current" | "pending" | "failed";
resourceBindings: Array<Record<string, unknown>>;
grants: MaterializedAuthorityGrants;
reconciledAt: string | null;
error?: string;
};
type AuthorityPlan = {
planId: string;
deploymentId: string;
classification: "update" | "migration";
proposal: ContractProposal;
desiredChange: Record<string, unknown>;
materializationPreview: Record<string, unknown>;
warnings: string[];
createdAt: string;
expiresAt?: string;
state?: "pending" | "accepted" | "rejected" | "expired";
decisionAt?: string | null;
decisionBy?: Record<string, unknown> | null;
decisionReason?: string | null;
};
type DeploymentPortalRoute = {
deploymentId: string;
portalId: string | null;
entryUrl: string | null;
disabled: boolean;
updatedAt: string;
};
type DeploymentAuthorityGrantOverride =
& {
deploymentId: string;
contractId: string;
grantKind: "capability" | "capability-group";
capability: string | null;
capabilityGroupKey: string | null;
}
& (
| {
identityKind: "web";
origin: string;
sessionPublicKey: null;
}
| {
identityKind: "session";
origin: null;
sessionPublicKey: string;
}
);
type ServiceInstance = {
instanceId: string;
deploymentId: string;
instanceKey: string;
disabled: boolean;
capabilities: string[];
resourceBindings?: Record<string, unknown>;
createdAt: string;
};
// Service instances record durable service identity and operator state only.
// Runtime bindings come from materialized authority.
type AuthDeployment =
| {
kind: "service";
deploymentId: string;
namespaces: string[];
disabled: boolean;
}
| {
kind: "device";
deploymentId: string;
reviewMode?: "none" | "required";
disabled: boolean;
};
type CreateDeploymentRequest =
| { kind: "service"; deploymentId: string; namespaces: string[] }
| { kind: "device"; deploymentId: string; reviewMode?: "none" | "required" };
type CreateDeploymentResponse = { deployment: AuthDeployment };
type ListDeploymentsRequest = {
kind?: "service" | "device";
disabled?: boolean;
offset?: number;
limit: number;
};
type ListDeploymentsResponse = PageResponse<AuthDeployment>;
type ListDeploymentAuthorityRequest = {
kind?: "service" | "device" | "app" | "cli" | "native" | "device-user";
disabled?: boolean;
offset?: number;
limit: number;
};
type ListDeploymentAuthorityResponse = PageResponse<DeploymentAuthority>;
type GetDeploymentAuthorityRequest = { deploymentId: string };
type GetDeploymentAuthorityResponse = {
authority: DeploymentAuthority;
materializedAuthority: MaterializedAuthority | null;
portalRoute: DeploymentPortalRoute | null;
grantOverrides: DeploymentAuthorityGrantOverride[];
};
type PlanDeploymentAuthorityRequest = {
deploymentId: string;
contract: Record<string, unknown>;
expectedDigest: string;
};
type PlanDeploymentAuthorityResponse = { plan: AuthorityPlan };
type ListAuthorityPlansRequest = {
deploymentId?: string;
state?: "pending" | "accepted" | "rejected" | "expired";
classification?: "update" | "migration";
kind?: "service" | "device" | "app" | "cli" | "native" | "device-user";
offset?: number;
limit: number;
};
type ListAuthorityPlansResponse = PageResponse<AuthorityPlan>;
type GetAuthorityPlanRequest = { planId: string };
type GetAuthorityPlanResponse = { plan: AuthorityPlan };
type AcceptAuthorityUpdateRequest = {
planId: string;
expectedDesiredVersion?: string;
};
type AcceptAuthorityMigrationRequest = {
planId: string;
expectedDesiredVersion?: string;
acknowledgement: string;
};
type AcceptAuthorityResponse = { authority: DeploymentAuthority };
type RejectAuthorityPlanRequest = { planId: string; reason?: string };
type RejectAuthorityPlanResponse = { success: boolean };
type ReconcileDeploymentAuthorityRequest = {
deploymentId: string;
desiredVersion?: string;
};
type ReconcileDeploymentAuthorityResponse = {
authority: DeploymentAuthority;
materializedAuthority: MaterializedAuthority;
};
type PutGrantOverridesRequest = {
deploymentId: string;
overrides: DeploymentAuthorityGrantOverride[];
};
type ListGrantOverridesRequest = {
offset?: number;
limit: number;
};
type ListGrantOverridesResponse = PageResponse<
DeploymentAuthorityGrantOverride
>;
type RemoveGrantOverridesRequest = {
deploymentId: string;
overrides: DeploymentAuthorityGrantOverride[];
};
type GrantOverridesResponse = {
grantOverrides: DeploymentAuthorityGrantOverride[];
}; Deployment authority RPCs:
Auth.DeploymentAuthority.Listpages deployment-owned desired authority.Auth.DeploymentAuthority.Getreturns desired deployment authority plus the current materialized authority view, portal routing, and grant overrides.Auth.DeploymentAuthority.Planderives a contract proposal from the presented contract, compares it with desired authority, and returns either an authority update or an authority migration plan, including incompatible same-contract replacement plans.Auth.DeploymentAuthority.Plans.Listpages pending and historical authority plans with optional deployment, state, classification, and kind filters.Auth.DeploymentAuthority.Plans.Getreturns one pending or historical authority plan.Auth.DeploymentAuthority.AcceptUpdateaccepts only plans classified as"update", mutates desired state, and schedules reconciliation after commit.Auth.DeploymentAuthority.AcceptMigrationaccepts only plans classified as"migration", requires explicit admin acknowledgement, and mutates desired state before scheduling reconciliation after commit.Auth.DeploymentAuthority.Rejectrejects a pending plan without mutating desired or materialized authority.Auth.DeploymentAuthority.Reconcileis an admin-triggered convergence request that materializes desired state into resources, bindings, and runtime grants.Auth.DeploymentAuthority.GrantOverrides.Putreplaces all grant override rows for one deployment.Auth.DeploymentAuthority.GrantOverrides.Listpages grant override rows across deployments.Auth.DeploymentAuthority.GrantOverrides.Removeremoves exact matching rows. Mutations return the deployment’s current grant override rows.
Rules:
ContractProposal.requestedNeedsandDeploymentAuthority.desiredState.needsare grouped bycontracts,surfaces,capabilities, andresourcesMaterializedAuthority.grantsis grouped bycapabilities,surfaces, andnats; it is a reconciled projection, not desired authority- accepting an authority update or authority migration approves desired authority changes, including resulting resource definition changes, and schedules reconciliation after the desired-state commit
- authority plans include pending requests and accepted or rejected history;
auto-accepted
mutable-devsame-contract replacement migrations remain visible with a recorded decision timestamp and auto-approval reason - reconciliation is the only path that creates, updates, removes, adopts, or purges materialized resources and bindings
- runtime bootstrap receives only current materialized authority where
materializedAuthority.status === "current"andmaterializedAuthority.desiredVersion === authority.version; if desired authority is accepted but not yet materialized, bootstrap waits or retries - stale or obsolete persisted materialized-authority projections are repaired by Trellis storage upgrade and reconciliation; repaired rows are not runtime permissions until they are current for the deployment authority version
- service and device deployment enable/disable mutations validate against staged deployment authority because enabled desired state determines what can later be materialized
- deployment authority mutations fail closed when required
usesdependencies are unknown or cannot be resolved from effective active contracts or the latest accepted dependency fallback - runtime credentials use materialized
natsgrants from current materialized authority instead of recomputing subjects from active contracts during auth-callout - service and device deployment removal may skip
usesvalidation so operators can tear down an already-broken graph instead of being trapped by stale dependencies - if a same-
contractIddigest is incompatible with the deployment’s latest accepted digest or offer, auth classifies the replacement as an authority migration. Understrict, auth records a pending migration plan and waits for explicit admin acceptance. Undermutable-dev, auth records the same migration plan, auto-accepts it with an auto-approval decision, mutates desired authority, and schedules reconciliation. - missing optional dependency contracts or optional requested surfaces are absent from the contract proposal and grant no authority; if they later become active, a fresh plan is required before a reconnect can use that optional authority
- the successful service bootstrap response includes the materialized resource binding payload for the presented digest; service runtimes use that binding to initialize KV, store, jobs, and transfer helpers without requiring a post-connect discovery RPC call from the service principal
type DisableDeploymentRequest = {
kind: "service" | "device";
deploymentId: string;
};
type EnableDeploymentRequest = {
kind: "service" | "device";
deploymentId: string;
};
type RemoveDeploymentRequest = {
kind: "service" | "device";
deploymentId: string;
cascade?: boolean;
// Also purge unused known-contract manifests for contract digests that are no
// longer referenced by any deployment authority, offer, or history record.
purgeUnusedContracts?: boolean;
};
type RemoveDeploymentResponse = { success: boolean };
type ProvisionServiceInstanceRequest = {
deploymentId: string;
instanceKey: string;
};
type ProvisionServiceInstanceResponse = { instance: ServiceInstance };
type ListServiceInstancesRequest = {
deploymentId?: string;
disabled?: boolean;
offset?: number;
limit: number;
};
type ListServiceInstancesResponse = PageResponse<ServiceInstance>;
type DisableServiceInstanceRequest = { instanceId: string };
type EnableServiceInstanceRequest = { instanceId: string };
type RemoveServiceInstanceRequest = { instanceId: string };
type RemoveServiceInstanceResponse = { success: boolean };
type DeviceInstance = {
instanceId: string;
publicIdentityKey: string;
deploymentId: string;
metadata?: Record<string, string>;
state: "registered" | "activated" | "revoked" | "disabled";
createdAt: string;
activatedAt: string | null;
revokedAt: string | null;
};
type DeviceActivationRecord = {
instanceId: string;
publicIdentityKey: string;
deploymentId: string;
activatedBy?: {
origin: string;
id: string;
};
state: "activated" | "revoked";
activatedAt: string;
revokedAt: string | null;
};
type DeviceActivationReview = {
reviewId: string;
instanceId: string;
publicIdentityKey: string;
deploymentId: string;
state: "pending" | "approved" | "rejected";
requestedAt: string;
decidedAt: string | null;
reason?: ActivationDecisionReason;
};
type DeviceConnectInfo = {
instanceId: string;
deploymentId: string;
contractId: string;
contractDigest: string;
transports: {
native?: { natsServers: string[] };
websocket?: { natsServers: string[] };
};
transport: {
sentinel: {
jwt: string;
seed: string;
};
};
auth: {
mode: "device_identity";
authority: "admin_reviewed" | "user_delegated";
iatSkewSeconds: number;
};
};
type ActivateDeviceRequest = {
flowId: string;
};
type ActivateDeviceProgress = {
status: "pending_review";
reviewId: string;
instanceId: string;
deploymentId: string;
requestedAt: string;
};
type ActivateDeviceResponse =
| {
status: "activated";
instanceId: string;
deploymentId: string;
activatedAt: string;
confirmationCode?: string;
}
| {
status: "rejected";
reason?: ActivationDecisionReason;
};
type WaitForDeviceActivationRequest = {
flowId: string;
publicIdentityKey: string;
nonce: string;
contractDigest: string;
iat: number;
sig: string;
};
type WaitForDeviceActivationResponse =
| { status: "pending" }
| {
status: "activated";
activatedAt: string;
confirmationCode?: string;
connectInfo: DeviceConnectInfo;
}
| {
status: "rejected";
reason?: ActivationDecisionReason;
};
type GetDeviceConnectInfoRequest = {
publicIdentityKey: string;
contractDigest: string;
iat: number;
sig: string;
};
type GetDeviceConnectInfoResponse = {
status: "ready";
connectInfo: DeviceConnectInfo;
};
// `POST /auth/devices/connect-info` and `Auth.Devices.ConnectInfo.Get` return
// `auth.authority: "user_delegated"` for activated devices and
// `auth.authority: "admin_reviewed"` for admin/review-approved setup flows.
// Runtime access still requires the presented contract to fit deployment
// authority and for required runtime bindings to be materialized.
//
// `POST /auth/devices/activate/wait` verifies the signed `flowId` and then
// loads the browser flow directly by that id before matching `publicIdentityKey`
// and `nonce`. The QR/MAC activation payload remains the intended bearer
// artifact for handing the setup flow from the device to a browser.
type ProvisionDeviceInstanceRequest = {
deploymentId: string;
publicIdentityKey: string;
activationKey: string;
metadata?: Record<string, string>;
};
type ProvisionDeviceInstanceResponse = { instance: DeviceInstance };
type ListDeviceInstancesRequest = {
deploymentId?: string;
state?: "registered" | "activated" | "revoked" | "disabled";
offset?: number;
limit: number;
};
type ListDeviceInstancesResponse = PageResponse<DeviceInstance>;
type DisableDeviceInstanceRequest = { instanceId: string };
type EnableDeviceInstanceRequest = { instanceId: string };
type RemoveDeviceInstanceRequest = { instanceId: string };
type RemoveDeviceInstanceResponse = { success: boolean };
type ListDeviceActivationsRequest = {
instanceId?: string;
deploymentId?: string;
state?: "activated" | "revoked";
offset?: number;
limit: number;
};
type ListDeviceActivationsResponse = PageResponse<DeviceActivationRecord>;
type RevokeDeviceActivationRequest = { instanceId: string };
type ListDeviceActivationReviewsRequest = {
instanceId?: string;
deploymentId?: string;
state?: "pending" | "approved" | "rejected";
offset?: number;
limit: number;
};
type ListDeviceActivationReviewsResponse = PageResponse<DeviceActivationReview>;
type DecideDeviceActivationReviewRequest = {
reviewId: string;
decision: "approve" | "reject";
reason?: ActivationDecisionReason;
};
type DecideDeviceActivationReviewResponse = {
review: DeviceActivationReview;
activation?: DeviceActivationRecord;
confirmationCode?: string;
}; Portal rules:
- Trellis always provides built-in login and generic device-activation portal routes; they are commonly served by the Trellis HTTP server from static assets and the built-in login portal is represented as a visible, non-deletable auth-owned portal record
- login portal records, policy, and route selection live in auth-owned projected
storage and are exposed through
Auth.Portals.*admin RPCs; device-activation portal routing remains deployment-owned unless its design explicitly changes - non-built-in login portal records can be created or updated through
Auth.Portals.Put; the built-in login portal remains visible and non-deletable and cannot be replaced by a portal upsert - non-built-in login portal records can be removed through
Auth.Portals.Removeonly when no login route targets them; built-in portal records cannot be removed - login portal settings include
allowedFederatedProviders: string[] | null;nullallows every configured OAuth/OIDC provider,[]allows no federated providers, and an array allows only that configured subset Auth.Portals.Getreturns the portal record, login settings, portal-scoped routes, default grants, andfederatedProvidersfor admin display of configured provider ids, display names, and provider types; it does not expose provider secrets or mutate provider configuration- custom portal apps should use explicit Trellis URL config rather than same-origin inference
- portal routing metadata does not imply consent, capabilities, or availability; delegated access comes from identity authority, deployment authority, and grant overrides
- custom portals remain first-class browser apps, but there is no portal-specific contract kind or portal-specific auth machinery
- if a portal later calls Trellis after bind, it does so as a normal user-authenticated browser app contract rather than through portal-specific contract handling
- portals MUST NOT use service deployment authority as their trust model
Portal routing rules:
- login flows resolve portal routing from auth-owned selectors in this order: contract id plus origin, contract id, origin, global default, built-in login portal fallback
- device activation resolves portal routing from deployment-owned device portal metadata, then falls back to the built-in Trellis device portal
- for login routes, the built-in login portal has the explicit id
trellis.builtin.login - the built-in device activation portal is a Trellis-owned app contract with the
id
trellis.portal.activation@v1 - custom login portals must have a visible portal record before a login route can target them
- route keys are selector-derived RPC identity for one
contractId + originselector; operator UI should present routes as app/contract selectors owned by each portal instead of as a global route inventory - adding a portal-scoped route for a selector already targeting another portal is rejected rather than silently moving the selector
- most deployments can rely on the built-in portal; custom routing is optional
Library rule:
- public client libraries MAY wrap these HTTP and RPC surfaces with higher-level
browser-flow, portal, admin, service, and device-activation helpers, but those
helpers MUST preserve these canonical wire shapes and the
Auth.DeviceUserAuthorities.Resolveoperation model - exact TypeScript helper declarations belong in the generated
/apireference; exact Rust helper declarations belong in Rustdoc
Device-activation observation rule:
- portal-side review state is observed through normal operation
progress,watch(), andwait()semantics onAuth.DeviceUserAuthorities.Resolve, not through a separate status-poll RPC - when
Auth.DeviceUserAuthorities.Reviews.Decideapproves or rejects a review, it completes the original device-user authority operation durably; retrying the decision is accepted only when the existing completed operation output matches the requested terminal result
Capability rule:
- review-decision RPCs MUST allow callers with
adminortrellis.auth::device.review - grant overrides are deployment metadata, not user-owned grants; user-facing callers still see only explicit user capabilities in insufficient-capability responses
- deployment grant override consent applies only when matching grant overrides themselves cover the required capabilities; user capabilities do not turn a grant override into deployment-owned authority
- portal routes, defaults, selections, and registration settings do not imply consent, service authority, or capability grants; registration availability is reported explicitly in browser-flow state
Canonical RPC inventory:
rpc.v1.Auth.Deployments.Createrpc.v1.Auth.Deployments.Listrpc.v1.Auth.Deployments.Disablerpc.v1.Auth.Deployments.Enablerpc.v1.Auth.Deployments.Removerpc.v1.Auth.DeploymentAuthority.Listrpc.v1.Auth.DeploymentAuthority.Getrpc.v1.Auth.DeploymentAuthority.Plans.Listrpc.v1.Auth.DeploymentAuthority.Plans.Getrpc.v1.Auth.DeploymentAuthority.Planrpc.v1.Auth.DeploymentAuthority.AcceptUpdaterpc.v1.Auth.DeploymentAuthority.AcceptMigrationrpc.v1.Auth.DeploymentAuthority.Rejectrpc.v1.Auth.DeploymentAuthority.Reconcilerpc.v1.Auth.DeploymentAuthority.GrantOverrides.Putrpc.v1.Auth.DeploymentAuthority.GrantOverrides.Listrpc.v1.Auth.DeploymentAuthority.GrantOverrides.Removerpc.v1.Auth.ServiceInstances.Provisionrpc.v1.Auth.ServiceInstances.Listrpc.v1.Auth.ServiceInstances.Disablerpc.v1.Auth.ServiceInstances.Enablerpc.v1.Auth.ServiceInstances.Removerpc.v1.Auth.Identities.Listrpc.v1.Auth.IdentityGrants.Listrpc.v1.Auth.IdentityGrants.Revokerpc.v1.Auth.Devices.Provisionrpc.v1.Auth.Devices.Listrpc.v1.Auth.Devices.Disablerpc.v1.Auth.Devices.Enablerpc.v1.Auth.Devices.Removerpc.v1.Auth.Devices.ConnectInfo.Getrpc.v1.Auth.DeviceUserAuthorities.Listrpc.v1.Auth.DeviceUserAuthorities.Revokerpc.v1.Auth.DeviceUserAuthorities.Reviews.Listrpc.v1.Auth.DeviceUserAuthorities.Reviews.Deciderpc.v1.Auth.Sessions.Listrpc.v1.Auth.Sessions.Logoutrpc.v1.Auth.Sessions.Merpc.v1.Auth.Sessions.Revokerpc.v1.Auth.Users.Listrpc.v1.Auth.Users.Getrpc.v1.Auth.Users.Createrpc.v1.Auth.Users.Updaterpc.v1.Auth.UserIdentities.Listrpc.v1.Auth.UserIdentities.Unlinkrpc.v1.Auth.Portals.Listrpc.v1.Auth.Portals.Getrpc.v1.Auth.Portals.Putrpc.v1.Auth.Portals.Removerpc.v1.Auth.Portals.LoginSettings.Getrpc.v1.Auth.Portals.LoginSettings.Updaterpc.v1.Auth.Portals.Routes.Putrpc.v1.Auth.Portals.Routes.Removerpc.v1.Auth.Users.IdentityLink.Createrpc.v1.Auth.Users.Password.Changerpc.v1.Auth.Users.PasswordReset.Createrpc.v1.Auth.Capabilities.Listrpc.v1.Auth.CapabilityGroups.Listrpc.v1.Auth.CapabilityGroups.Getrpc.v1.Auth.CapabilityGroups.Putrpc.v1.Auth.CapabilityGroups.Delete
Canonical operation inventory:
operations.v1.Auth.DeviceUserAuthorities.Resolve
Canonical event inventory:
events.v1.Auth.Connections.Openedevents.v1.Auth.Connections.Closedevents.v1.Auth.Connections.Kickedevents.v1.Auth.Sessions.Revokedevents.v1.Auth.DeviceUserAuthorities.Requestedevents.v1.Auth.DeviceUserAuthorities.Approvedevents.v1.Auth.DeviceUserAuthorities.ReviewRequestedevents.v1.Auth.DeviceUserAuthorities.Resolved
Admin RPCs
Admin RPCs require the admin capability unless explicitly documented
otherwise. Device review decision RPCs are the current exception and also allow trellis.auth::device.review.
Admin list RPCs are bounded production queries using the standard live offset
page shape. Requests are { offset?: number; limit: number } plus documented
filters. Responses are { entries, count, offset, limit, nextOffset? }. This is
live offset pagination, not snapshot or cursor pagination: concurrent inserts or
deletes can change which rows appear at later offsets. Admin list RPCs MUST NOT
expose an unbounded “list all” mode.
rpc.Auth.Sessions.List
Request:
{
user?: string;
offset?: number;
limit: number;
} Response:
type AuthSessionRow =
| {
key: string;
sessionKey: string;
participantKind: "app" | "agent";
principal: {
type: "user";
userId: string;
name: string;
identity: { identityId: string; provider: string; subject: string };
};
contractId: string;
contractDisplayName: string;
createdAt: string;
lastAuth: string;
}
| {
key: string;
sessionKey: string;
participantKind: "device";
principal: {
type: "device";
deviceId: string;
deviceType: string;
deploymentId: string;
runtimePublicKey: string;
};
contractId: string;
contractDisplayName?: string;
createdAt: string;
lastAuth: string;
}
| {
key: string;
sessionKey: string;
participantKind: "service";
principal: {
type: "service";
id: string;
instanceId: string;
deploymentId: string;
name: string;
};
createdAt: string;
lastAuth: string;
};
{
entries: AuthSessionRow[];
count: number;
offset: number;
limit: number;
nextOffset?: number;
} rpc.Auth.Users.List
Request:
{
offset?: number;
limit: number;
} Response:
type AuthUserRow = {
userId: string;
name?: string;
email?: string;
active: boolean;
capabilities: string[];
capabilityGroups: string[];
identities: Array<{
identityId: string;
provider: string;
subject: string;
displayName: string | null;
email: string | null;
emailVerified: boolean;
linkedAt: string;
lastLoginAt: string | null;
}>;
};
{
entries: AuthUserRow[];
count: number;
offset: number;
limit: number;
nextOffset?: number;
} Auth.Users.Create uses the same account fields except userId and identities: callers may supply name, email, active, direct capabilities, capabilityGroups, and optional username; Trellis always
generates the canonical userId. When username is present, Trellis atomically
creates the account and its initial local username identity. Each Trellis
account may have at most one local username/password identity; it may have many
linked OIDC identities.
Trellis-generated user ids use the usr_ prefix followed by a ULID. If local
username creation fails because the username already belongs to another local
identity, Auth.Users.Create returns an AuthError with reason: "username_taken" and a human-readable message. Generated user-id
collisions are unexpected internal failures, not user-actionable form errors.
Admin bootstrap creates or reuses the initial local admin account and local
identity before issuing a password-reset URL for that identity. Bootstrap admin
accounts assign the built-in admin group by storing capabilityGroups: ["admin"]; they do not copy the admin group’s current
capabilities into the account’s direct capabilities grant. Older accounts may
still carry direct "admin" grants; authorization resolves both direct
capabilities and assigned groups.
rpc.Auth.Capabilities.List
Request:
{
offset?: number;
limit: number;
} Response:
type AuthCapabilityRow = {
key: string;
displayName: string;
description: string;
consequence?: string;
source: "contract" | "platform";
contractId?: string;
contractDigest?: string;
contractDisplayName?: string;
};
{
entries: AuthCapabilityRow[];
count: number;
offset: number;
limit: number;
nextOffset?: number;
} Rules:
Auth.Capabilities.Listreturns the assignment catalog known to the current auth runtime: Trellis platform capabilities plus capability metadata projected into authority-owned capability definitions when deployment authority is planned or accepted. Durable deployment authority and identity authority remain the authority sources.- The response is an assignment catalog for admin UX; it is not a grant source by itself.
- Capability keys are canonical global keys such as
trellis.auth::device.review; contract-owned keys originate from declared top-level capability metadata, while platform keys are explicitly defined by Trellis.
rpc.Auth.Users.Update
Request:
{
userId: string;
active?: boolean;
capabilities?: string[];
capabilityGroups?: string[];
name?: string;
email?: string;
} Response:
{
success: boolean;
} Rules:
capabilities, when present, replaces the user’s explicit capability grants with the exact canonical keys supplied by the admin caller.capabilityGroups, when present, replaces the user’s assigned dynamic group keys. Groups are resolved at authorization time; direct capabilities are kept as explicit per-user grants.- Unknown or uncataloged existing capability strings may remain on a user
record, but new Trellis-owned assignments SHOULD use keys returned by
Auth.Capabilities.List.
rpc.Auth.Users.IdentityLink.Create
Request:
{} Response:
{
flowId: string;
url: string;
expiresAt: string;
} Rules:
- this is a self-service authenticated-user RPC with no capability requirement
- the flow always targets the caller’s own
userId - callers cannot pass another account id or provider filters
- the returned
urlis intended for clients such as the Console profile to open the account-link flow directly; users should not need to copy a generated link by hand - the returned
flowIdis a ULID - completing the flow may add another OIDC identity to the account
- completing a local username/password link is allowed only when the target account has no existing local identity
- admins may view and unlink user identities through management surfaces, but do not generate identity-link URLs for other users
rpc.Auth.Users.Password.Change
Request:
{
currentPassword: string;
newPassword: string;
} Response:
{
success: boolean;
} Rules:
- this is a self-service authenticated-user RPC with no capability requirement
- the target is always the caller’s own
userId; callers cannot pass another account id - the target account must have exactly one local identity and an existing local credential
- Trellis verifies
currentPasswordagainst the existing local credential before replacing it withnewPassword - successful password change revokes other active sessions for the target user
when session storage is available, while preserving the caller’s current
session when the RPC context includes
sessionKey
rpc.Auth.Users.PasswordReset.Create
Request:
{
userId: string;
expiresInSeconds?: number;
} Response:
{
flowId: string;
url: string;
expiresAt: string;
} Rules:
- this is an admin RPC and requires the
admincapability - the flow targets the supplied
userId - the returned
flowIdis a ULID - the target user must already have exactly one local identity; reset creation fails rather than allowing the link holder to choose a username
- the same durable flow kind covers first-time credential setup for an existing local identity and later password reset
- completion is local-provider only and creates or replaces the bound local identity’s credential
- successful completion revokes active sessions for the target user
expiresInSecondsis bounded by auth policy; omitted values use the default account-flow TTL
rpc.Auth.CapabilityGroups.*
Capability groups are admin-managed dynamic authorization inputs. Assigning a
group stores the group key on the user account; it does not copy the group’s
current capabilities into the user’s direct grants. The built-in admin group
is read-only in management surfaces, but can be assigned to users.
Auth.CapabilityGroups.Put accepts only capability keys returned by Auth.Capabilities.List, including Trellis platform keys and
authority-projected contract capability definitions. Requests that include
uncataloged capability strings fail with invalid_request instead of preserving
or creating hidden grants.
Admin UX SHOULD make the distinction visible: capabilities provided by selected groups should appear resolved for review but should not be editable as direct grants unless the group is removed from the user.
rpc.Auth.Sessions.Revoke
Request:
{
sessionKey: string;
} Response:
{
success: boolean;
} Emitted Events
Trellis publishes these events as part of trellis.auth@v1:
events.v1.Auth.Connections.Openedevents.v1.Auth.Connections.Closedevents.v1.Auth.Sessions.Revokedevents.v1.Auth.Connections.Kickedevents.v1.Auth.DeviceUserAuthorities.Requestedevents.v1.Auth.DeviceUserAuthorities.ReviewRequestedevents.v1.Auth.DeviceUserAuthorities.Approvedevents.v1.Auth.DeviceUserAuthorities.Resolved
Services may subscribe only when the presented contract fits materialized
authority and declares the events in grouped uses.required or uses.optional entries that are active and authorized.
Non-Goals
- defining the proof/signature protocol
- defining TypeScript or Rust helper packages
- deployment/runbook guidance