Appearance
Bootstrap-tokens HTTP API
This is the reference for the bootstrap-tokens HTTP surface. It maps each operation to its OpenAPI schema, the per-call ReBAC permission gate (or, for the registration seam, the BootstrapToken precedence check that replaces it), the audit-row taxonomy, and the closed Problem.code set the handlers emit.
The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract. For the bounded-context narrative (the psb_<env>_<token-id>_<kind>_<random> plaintext format, the Argon2id hash-only persistence, the one-shot/idempotent consume protocol, the registration five-step atomic commit, the IP allocator semantics, the NSK issuance and wrap ledger) see ../../contexts/identity/bootstrap-tokens.md and ../../contexts/identity/registration.md. For operator recipes see ../../how-to/enrolment/issue-a-bootstrap-token.md and ../../how-to/enrolment/register-a-node.md.
Operations
| Method | Path | Operation ID | ReBAC gate | Audit relation | Body cap |
|---|---|---|---|---|---|
| POST | /v1/projects/{project_id}/bootstrap-tokens | IssueBootstrapToken | project#manage on parent Project | bootstrap_token.issue | 8 KiB |
| GET | /v1/projects/{project_id}/bootstrap-tokens | ListBootstrapTokens | project#read (gate) + per-row bootstrap_token#read filter | bootstrap_token.list | n/a |
| GET | /v1/projects/{project_id}/bootstrap-tokens/{id} | GetBootstrapTokenMetadata | bootstrap_token#read | bootstrap_token.read | n/a |
| DELETE | /v1/projects/{project_id}/bootstrap-tokens/{id} | RevokeBootstrapToken | bootstrap_token#manage (or project#manage) | bootstrap_token.revoke | n/a |
| POST | /v1/register | PostRegister | (none — BootstrapToken plaintext is the credential) | node.register | 8 KiB |
IssueBootstrapTokenreturns the plaintext exactly once in the 201 response. Persistence stores only the Argon2id hash; once the response body has been written the plaintext can no longer be retrieved through any API surface. The Spectralplexsphere-write-once-post-must-be-issue-responserule guards thetokenfield'sx-plexsphere-once: truemarker.RevokeBootstrapTokenis a terminal aggregate transition — a token that is already consumed or already revoked rejects the call with409so the operator can distinguish "I beat the redeemer" from "the redeemer beat me".PostRegisteris the unauthenticated bootstrap seam. The BootstrapToken plaintext IS the credential; the caller does NOT pass anAuthorizationheader. Denials surface from BootstrapToken validation (aProblem, NOT aPermissionDenied), not from a ReBAC check. The endpoint sits on the authn middleware bypass list for the same reason/v1/auth/sign-indoes — a substrate without credentials must be able to reach it in order to obtain them.
Issuance invariants
The aggregate enforces three issuance invariants on IssueBootstrapToken:
| Field | Constraint | Failure |
|---|---|---|
kind | Must be one of node, bridge. | 400 invalid_kind |
env_prefix | Matches ^[a-z]+$. | 400 invalid_env_prefix |
ttl_seconds | Inside [300, 86400]. | 400 invalid_ttl |
Violations surface as 400 from validation, not as a SQL CHECK failure — the validator runs before any database access, so a bad issuance request never produces a partially-persisted aggregate.
Registration atomic commit
PostRegister performs the following in one pgx transaction:
- Public-key gate. The inbound 32-byte X25519 public key is validated for length (after base64 decode) and rejected if it matches the all-zero small-order point. This gate runs before any token consumption attempt so a malformed key cannot also waste a token.
- BootstrapToken consume. The plaintext is run through
bootstraptokens.Validator.Consumefor one-shot, project-scoped, kind-scoped enforcement. A canonical denial reason (see error taxonomy) surfaces as403. - Mesh-IP allocation. Drawn from the Domain pool by default, from the optional Project sub-range when one applies. Pool exhaustion or allocator contention surfaces as
503and does NOT consume the BootstrapToken. - NSK issuance. The per-Node Node Secret Key plaintext (32 bytes, base64) plus the wrapped persistence form is produced through the configured wrap-key adapter. The plaintext is returned to the caller exactly once (
x-plexsphere-once: true). - Persist. Node + IP allocation + NSK + outbox event are persisted in the same transaction. A partial failure rolls back every step including the BootstrapToken consume, so the operator can retry the same plaintext after fixing the root cause.
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| every Issue/List/Get/Revoke | project_id (path) | string (uuid) | yes | Owning Project (UUIDv7). Shared BootstrapTokenProjectID parameter component. Malformed → 400 invalid_project_id. |
| Get / Revoke | id (path) | string (uuid) | yes | BootstrapToken identifier (UUIDv7). |
| ListBootstrapTokens | cursor (query) | string | no | Opaque HMAC-signed continuation. Tampered → 400 invalid_cursor. |
| ListBootstrapTokens | limit (query) | integer | no | [1, 200], default 50. Out-of-range → 400 invalid_limit. |
Schemas
The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:
- Issuance:
BootstrapTokenIssueRequest,BootstrapTokenIssueResponse(plaintext one-shot),BootstrapTokenMetadata(read view),BootstrapTokenList. - Registration:
RegisterRequest,RegisterResponse(carriesnode_id,mesh_ip,signing_public_key,signing_key_id,nsk,peer_snapshot,domain_mesh_cidr),RegisterPeer.
The RegisterResponse.nsk and the BootstrapTokenIssueResponse.token fields are the only /v1 fields marked x-plexsphere-once: true; the Spectral guard rule keeps the marker pinned so a future spec edit cannot accidentally relax the one-shot semantic.
Error taxonomy
All error responses use the shared Problem envelope (application/problem+json). The 403 path on the authenticated operations uses the richer PermissionDenied shape carrying the ReBAC denial reason, traversed relation_path, and request correlation_id. PostRegister is the exception: its 403 body is a plain Problem because the denial originates from BootstrapToken validation, not from a ReBAC check.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_project_id | 400 | every operation | Malformed project_id UUID. |
invalid_kind | 400 | Issue | kind not in {node, bridge}. |
invalid_env_prefix | 400 | Issue | env_prefix does not match ^[a-z]+$. |
invalid_ttl | 400 | Issue | TTL outside [300, 86400]. |
invalid_cursor / invalid_limit | 400 | List | Pagination guards. |
public_key_invalid | 400 | Register | Public key length wrong, all-zero, or unparseable. BootstrapToken NOT consumed. |
kind_mismatch | 403 | Register | Token kind does not match the redemption attempt. |
project_mismatch | 403 | Register | Token issued for a different Project. |
token_consumed | 403 | Register | Plaintext was already redeemed. |
token_expired | 403 | Register | TTL elapsed. |
token_revoked | 403 | Register | Operator revoked the token before redemption. |
nonce_collision | 403 | Register | Replay nonce already used inside the Project. |
not_found | 404 | Get / Revoke / Register | BootstrapToken, Project, or Resource not resolvable. The Register branch does NOT consume the token. |
token_terminal | 409 | Revoke | Token is already consumed or revoked. |
register_invalid | 422 | Register | Body parsed but failed application-boundary invariants (empty plaintext, zero project_id, empty resource_id). |
pool_exhausted / subrange_exhausted / allocator_contention | 503 | Register | Mesh-IP allocator could not place the Node. BootstrapToken NOT consumed. |
internal | 500 | every operation | Server-side failure path. NSK issuance, outbox append, or any other infrastructure fault. |
token_consumed and nonce_collision are deliberately surfaced as 403 rather than 409 so the redeemer cannot distinguish "I have already used this token" from "someone else already used this token" via the status code alone — the canonical denial reason in Problem.code is the disambiguation, but only callers who already know the plaintext can interpret it.
Cross-references
../../contexts/identity/bootstrap-tokens.md— bounded-context reference for the BootstrapToken aggregate, the plaintext format, the Argon2id hash-only persistence, the one-shot Validator/Consume protocol, and the issuance invariants.../../contexts/identity/registration.md— bounded-context reference for the registration five-step atomic commit, the NSK issuance and wrap ledger, the IP allocator semantics, and the threat model for the unauthenticated bootstrap seam.../../how-to/enrolment/issue-a-bootstrap-token.md— operator recipe for issuing a BootstrapToken via plexctl.../../how-to/enrolment/register-a-node.md— operator recipe for redeeming a BootstrapToken end-to-end against the kind dev, including the X25519 keypair generation, the NSK persistence, and the verification checklist.../../../api/openapi/plexsphere-v1.yaml— OpenAPI 3.1 spec; theBootstrapToken*andPostRegisteroperations and theBootstrapTokenIssueRequest/BootstrapTokenIssueResponse/BootstrapTokenMetadata/BootstrapTokenList/RegisterRequest/RegisterResponse/RegisterPeerschemas.../../../tests/e2e/identity/register/— Chainsaw e2e suite that issues + redeems + replays a Node registration end-to-end, including the ciphertext / plaintext round-trip on a fixture Project secret.