Deploy access control
Covers the 5 API operations in the access category.
| Method | Path | Description |
|---|---|---|
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/access | Deploy access policy (runlot access) |
| PUT | /v1/orgs/{orgSlug}/projects/{projectName}/access | Update the policy (runlot access set) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/access/bypass | Issue a bypass key for automation (runlot access bypass --new) |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/access/bypass | Revoke the bypass key (runlot access bypass --revoke) |
| POST | /v1/access/authorize | One-time code for reaching a protected deploy (docs/access.md §3.4-3) |
GET /v1/orgs/{orgSlug}/projects/{projectName}/access
Who can reach this deploy (docs/access.md §3.1). A project that has never set a policy is public, not a 404 — most projects are in that state.
No values are returned. The password never leaves on any surface, and the bypass secret is shown only once, when it is created.
Requires viewer or above — "is this deploy open" is a fact anyone in the organization needs to know.
operationId getAccess
| Status | Description | Response body |
|---|---|---|
| 200 | Policy | AccessPolicy |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — no cp-core connection | Error |
PUT /v1/orgs/{orgSlug}/projects/{projectName}/access
public keeps today's behaviour, org requires member or above in the org that owns the project, and password is one shared password per project — the moment it becomes per-user it is a subscriber-table problem and outside the scope of this API (docs/access.md §3.1).
If you send no password, the existing one stays. If a save that only changes the mode wiped the password, you would have to set it again on the way back, and that inconvenience buys no safety. To clear it, send a new value.
Requires admin or above. A boundary a member can change is not a boundary. Audit event access.set (the password is not recorded in the audit).
operationId setAccess
Request body: application/json · object
| Status | Description | Response body |
|---|---|---|
| 200 | The updated policy | AccessPolicy |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/access/bypass
The key CI uses to reach a protected deploy (docs/access.md §3.6). Send it in the request header Runlot-Access-Bypass: <secret>.
The value exists only in this response. You cannot see it again, and calling again kills the old value immediately — that is the only way to invalidate a leaked secret.
Without this key, CI cannot reach a protected deploy and you may end up turning protection off. Requires admin or above. The audit event access.bypass.new does not record the key value.
operationId newAccessBypass
| Status | Description | Response body |
|---|---|---|
| 200 | Bypass key (shown once) | AccessBypass |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core | Error |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/access/bypass
Returns 204 even when there is no bypass key. The access policy itself does not change; the bypass key is managed separately from the policy. Requires admin or above. Audit event access.bypass.revoke.
operationId revokeAccessBypass
| Status | Description | Response body |
|---|---|---|
| 204 | Revoked | — |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core | Error |
POST /v1/access/authorize
This is the middle of the login round trip. When a request to a protected deploy comes back to the dashboard's /access/authorize screen, that screen makes this call with the session and moves to the redirect it receives. It is the front that sets the cookie on that callback — the fact that it cannot be set from another origin shaped this entire flow.
to must be a hostname we know. If we do not know it, the answer is 404: without this check the endpoint would be an open redirect that sends a logged-in user to any domain, and that domain would receive the code. next also accepts a path only (//evil.example is read as an absolute address).
Someone outside the org gets 403 — re-logging in does not change the result (§3.7). The point is that it requires viewer or above, not member or above: viewing a deploy is exactly a viewer's job.
§3.4-2 of the design document describes this flow as a browser GET /access/authorize. The dashboard stores the session token in localStorage, so the server cannot see the session on that GET. Only the party that performs the move changed from the server to the browser; the decision itself is unchanged.
operationId authorizeAccess
Request body: application/json · object
| Status | Description | Response body |
|---|---|---|
| 200 | Callback URL | AccessAuthorize |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core | Error |