PageSourceSearch

https://envless.cloud/assets/authentication._KF15LEo.js

js envless.cloud collected 2026-10-08 19:59:17 UTC 68,663 bytes, 59 lines download raw bytes

1import{j as e}from"./jsx-runtime.u17CrQMm.js";const l={title:"Authentication",description:"Pass a personal access key as a bearer token. That is the whole auth model."},c="\n<p>Every request must include an `Authorization` header carrying a personal access key (`ev_sk_...`).</p>\n\n```bash\nAuthorization: Bearer ev_sk_xxxxxxxxxxxx\n```\n\n<p>The `Bearer ` prefix is stripped when present and is otherwise optional, so a bare `Authorization: ev_sk_...` is accepted too. What is not optional is the `ev_sk_` prefix: a value that does not start with it is rejected before any database lookup happens.</p>\n\n<p>Generate keys at [envless.cloud/dashboard/keys](/dashboard/keys). The full key is shown once. Copy it into your secret store immediately. See [Keys](/docs/auth/keys) for naming, rotation, and revocation.</p>\n\n<Note>\n    A CLI session token (`env_cli_...`) is a different credential and is refused on these endpoints. The two families never overlap: the REST API rejects anything that is not `ev_sk_`, and the dashboard and `/cli/*` endpoints reject an `ev_sk_` key with `auth.api_key_not_accepted`.\n</Note>\n\n<p>The same routes are served twice, once at the root and once under `/v1`, so `https://api.envless.cloud/workspace` and `https://api.envless.cloud/v1/workspace` are the same endpoint.</p>\n\n## The key is the whole check\n\n<Warning>\n    **A key never presents a code, of either kind.** Neither the [two factor](/docs/auth/two-factor) sign-in challenge nor [step-up verification](/docs/auth/step-up) is ever put to this surface. A key that has the scope performs the operation, including the ones the dashboard guards: deleting another key or rewriting its scopes, removing a member, deleting a project, rolling a version back, creating a webhook endpoint and rotating its signing secret. The owner turning two factor on for their own account adds no challenge here and revokes nothing.\n</Warning>\n\n<Warning>\n    **What can stop a live key is a workspace that requires two factor.** A workspace owner or admin can require the second factor of every member. While that requirement is on, a key whose **owner** has not enabled two factor on their account is refused on every route, in the auth middleware, before scopes are read: `403` with `auth.workspace_two_factor_required`. That is enforceable on a credential that cannot answer a challenge because the question is asked of the owner's account record rather than of the request. Nothing is deleted, deactivated or re-scoped, and the key answers normally again the moment its owner enrolls or the requirement is turned off. See [Requiring it across a workspace](/docs/auth/two-factor#requiring-it-across-a-workspace).\n</Warning>\n\n<p>That is structural rather than an omission. Both of those checks are answered by a human at a keyboard: one reads a code out of an inbox, the other reads it off a phone. An `ev_sk_` key is a pre-authorized machine credential running unattended in CI, so there is nobody to ask, and gating the surface would break every consumer that uses it. The gates sit on **minting** instead: creating a key in the dashboard, rotating one, deleting one, and changing its scopes or allowlist all require step-up, and the account that does the minting can be second-factor protected. The workspace requirement above is the one gate that reaches a key already in use, and it manages that by asking a question about the owner's account rather than about the request.</p>\n\n<p>What follows from that is the control model. A key's blast radius is its scopes and its project allowlist, nothing else, and the only revocation is deletion or rotation from the dashboard. Scope keys to the minimum, keep one key per consumer so a single revocation is targeted, and treat a leaked key as fully live until the moment somebody deletes it.</p>\n\n## What a key grants\n\n<p>Every key belongs to a single workspace and carries a **scope per resource**. Each resource is granted one of three levels (`none`, `read`, or `write`), and `write` implies `read`. There are eleven resources:</p>\n\n| Resource | `read` | `write` |\n|---|---|---|\n| `workspace` | `GET /workspace`, `GET /workspaces`, `GET /workspace/domain` | update / delete workspace, create workspace, claim / verify / remove custom domain |\n| `products` | list / get products, list a product's projects | create / update / delete / duplicate |\n| `product-access` | list a product's access grants | grant / revoke member access |\n| `projects` | list / get | create / update / delete / duplicate |\n| `environments` | list / get | create / update / delete / duplicate / expose |\n| `variables` | list / get (ciphertext), history | create / update / delete (single, bulk, apply) |\n| `versions` | list / get, list entr
1ies | create / rollback / re-encrypt entries |\n| `members` | list members, list invites | invite / update role / remove / leave, resend / revoke invite |\n| `roles` | list / get roles, list permissions | create / update / delete roles |\n| `keys` | list / get | create / update / delete |\n| `webhooks` | list / get, event types, deliveries | create / update / delete, rotate secret, redeliver |\n\n<p>A resource left at `none` returns `403` on every route that names it. Some routes require two scopes at once, and both are checked before the handler runs: every duplicate route also needs `variables:write` (a product duplicate additionally needs `projects:write`), version rollback and entry re-encryption need `variables:write`, reading a version or its entries needs `variables:read`, and `GET /products/:slug/projects` needs `projects:read`.</p>\n\n<p>A key can additionally be pinned to an **allowlist of projects**. Leave it empty for the whole workspace, or list specific project IDs (not slugs) to fence the key in.</p>\n\n<p>What the fence narrows is `environments`, `variables`, `versions` and the project reads: `GET /projects` and `GET /products/{slug}/projects` return only the fenced projects, and any route resolving a project outside the fence is `403` `auth.key_project_restricted`.</p>\n\n<p>The workspace-wide surfaces are not narrowed, they are closed. Each of these refuses a fenced key outright with `403` `auth.key_project_restricted`, whatever scope the key holds:</p>\n\n| Refused for a fenced key | Why |\n|---|---|\n| `GET /keys`, `GET /keys/{id}`, `PATCH /keys/{id}`, `DELETE /keys/{id}` | Keys are workspace-wide, so there is no project subset of the key list to hand back |\n| Every webhook route, including the reads and `GET /webhooks/event-types` | Endpoints subscribe to workspace-wide events and have no project dimension |\n| `POST /products`, `PATCH` and `DELETE /products/{slug}`, `POST /products/{slug}/duplicate`, and `PUT` and `DELETE /products/{slug}/access/{memberId}` | A product spans projects the fence may not cover |\n| `POST /projects` and `POST /projects/{slug}/duplicate` | The new project could not be covered by the allowlist |\n\n<p>`POST /keys` is the one key route a fenced key keeps. There `projectIds` becomes mandatory and must be a non-empty subset of the caller's own fence, so a fenced key can mint narrower keys but can never read back the list it just added to.</p>\n\n<p>Everything else at workspace level is untouched by the fence, because none of it has a project dimension to narrow: `GET /me`, the workspace and domain routes, `GET /workspaces`, all of `members`, `invites` and `roles`, and the product reads `GET /products`, `GET /products/{slug}` and `GET /products/{slug}/access`.</p>\n\n<Note>\n    Scope is checked in middleware and the fence inside the handler, so the two errors are ordered. A fenced key without `keys:read` gets `403` `auth.scope_missing` on `GET /keys`; add the scope and the same key gets `403` `auth.key_project_restricted`. Granting the scope never opens a closed route.\n</Note>\n\n<p>Grant the narrowest scopes that get the job done. A CI key that only pulls ciphertext needs nothing beyond `variables:read`: the variables routes take the project and environment slugs in the path and resolve them internally, so no `projects` or `environments` scope is involved. Add `workspace:read` only if the client has to resolve the workspace ID itself. `@goenvless/env/edge` is the common case: its `load({ token, project, environment, passphrase }
1)` calls `GET /workspace` first whenever no `workspaceId` and no `key` were passed, because the passphrase is derived against the workspace ID. Pass `workspaceId` or a raw `key` and that call disappears, leaving `variables:read` as the only scope the edge loader needs.</p>\n\n<Note>\n    A key created without a `scopes` object gets **every scope set to `none`**. It can authenticate but not touch any resource until you assign scopes. Set them when you create the key.\n</Note>\n\n## Examples\n\n<p>`GET /me` is the only endpoint that requires no scope at all, which makes it the way to prove a key is live before you debug anything else. It returns the owning user, the bound `workspaceId` and the key's own `id`, `name` and `createdAt`.</p>\n\n<CodeGroup>\n```bash curl\ncurl https://api.envless.cloud/me \\\n    -H \"Authorization: Bearer ev_sk_xxxxxxxxxxxx\"\n```\n\n```ts fetch\nconst res = await fetch('https://api.envless.cloud/me', {\n    headers: { Authorization: `Bearer ${process.env.ENVLESS_TOKEN}` },\n});\nconst { data } = await res.json();\n```\n\n```python python\nimport os, requests\nres = requests.get(\n    \"https://api.envless.cloud/me\",\n    headers={\"Authorization\": f\"Bearer {os.environ['ENVLESS_TOKEN']}\"},\n)\ndata = res.json()[\"data\"]\n```\n\n```ruby ruby\nrequire \"net/http\"\nrequire \"json\"\n\nuri = URI(\"https://api.envless.cloud/me\")\nheaders = {\"Authorization\" => \"Bearer #{ENV.fetch(\"ENVLESS_TOKEN\")}\"}\ndata = JSON.parse(Net::HTTP.get_response(uri, headers).body)[\"data\"]\n```\n\n```php php\n$context = stream_context_create(['http' => [\n    'header' => 'Authorization: Bearer ' . getenv('ENVLESS_TOKEN'),\n]]);\n$data = json_decode(file_get_contents('https://api.envless.cloud/me', false, $context), true)['data'];\n```\n\n```go go\nreq, _ := http.NewRequest(\"GET\", \"https://api.envless.cloud/me\", nil)\nreq.Header.Set(\"Authorization\", \"Bearer \" + os.Getenv(\"ENVLESS_TOKEN\"))\nres, _ := http.DefaultClient.Do(req)\n```\n</CodeGroup>\n\n<p>The [TypeScript](/docs/sdk/overview), [Python](/docs/python/overview), [Ruby](/docs/ruby/overview) and [PHP](/docs/php/overview) SDKs send the header for you, and this whole call is `envless.me.get()` in the first two, `Envless.me.get` in Ruby and `$envless->me->get()` in PHP.</p>\n\n<p>Every response is the same envelope: `success`, `code`, `message`, `error`, `data` and `_req`. On a failure `error.code` carries the machine-readable string from the tables below, and `data` is `null`.</p>\n\n## Failure modes\n\n| Status | `error.code` | Meaning | Fix |\n|---|---|---|---|\n| `401` | `auth.api_key_required` | No `Authorization` header, or the value does not start with `ev_sk_`. | Add or fix the header. |\n| `401` | `auth.api_key_not_found` | Key was deleted, rotated, deactivated, or never existed. | Generate a new one and update your secret store. |\n| `401` | `auth.api_key_user_missing` | The key is valid but its owner account no longer exists. | Create the key again under a live account. |\n| `401` | `auth.api_key_owner_not_member` | The key's owner has left or been removed from the workspace. | Re-add the owner, or issue the key from a current member. |\n| `403` | `auth.workspace_two_factor_required` | The key's workspace requires two factor authentication and the key's owner has not enabled it on their account. Refused before scopes are read, on every route. | The owner turns two factor on from [Account settings](/dashboard/account), or someone with `workspace:update` turns the requirement off. |\n| `403` | `auth.scope_missing` | The key is valid but lacks the scope the endpoint needs. The message and `error.resource` name it as `resource:level`. | Add the scope to the key in the dashboard, or use a key that has it. |\n| `403` | `auth.key_project_restricted` | The key is fenced to specific projects and the request touched something outside them, or touched a closed workspace-wide surface: any key route other than `POST /keys`, any webhook route, a product or product-access write, or project creation and duplication. | Use a key with no project fence. On `POST /keys`, send a non-empty `projectIds` subset of the caller's own fence. |\n| `403` | `auth.product_restricted` | The key owner has no grant on the private product the target project belongs to. | Grant the owner access to the product. |\n| `403` | `auth.scope_ceiling_exceeded` | `POST /keys` asked for a `resource:level` above what the calling key holds. Nothing is created. | Request scopes at or below the calling key's own. |\n| `402` | `plan.upgrade_required` | The target project is locked because it is over the plan's project limit. | Upgrade the plan, or delete projects until the target is unlocked. |\
1n\n<Warning>\n    Never put a key in client-side code, public repos, build logs, or chat. Scope every key to the minimum it needs: a leaked key can do exactly what its scopes allow until you revoke it, so a read-only key limits the blast radius.\n</Warning>\n\n## Last used\n\n<p>Each key records a single `lastUsedAt` timestamp, shown in the dashboard, and nothing else. The write is throttled: a key that was already touched within the last 60 seconds is not written again, and the update is fire-and-forget so a failed write never fails the request. There is no per-key request counter and no per-request log to read back, so treat `lastUsedAt` as a liveness signal for finding dormant keys to rotate, not as usage accounting.</p>\n\n## Keys change underneath you\n\n<p>Two things change what a key can do without anyone touching the key. A plan change is not one of them: keys work on every plan, Free included, so a downgrade never deactivates a key, though a project the downgrade locks still answers `402` to it (see [Plans and billing](/docs/manage/plans-and-billing)).</p>\n\n<p>**A role change re-clamps the scopes of every key its owner holds in that workspace.** Scopes are clamped to the owner's workspace permissions at creation time and again whenever the owner's role, or that role's permission set, changes. A key can silently drop from `write` to `read`, or to `none`, so read `scopes` back rather than assuming the key still holds what you asked for.</p>\n\n<p>**A workspace two factor requirement stops every key whose owner has not enrolled.** Turning the requirement on is an admin action taken in the dashboard, and it lands on the keys immediately: every request returns `403` `auth.workspace_two_factor_required` until that owner enables two factor or the requirement comes back off. The key's record is not changed, so nothing in `GET /keys` looks different. If a pipeline stops the same day someone tightened workspace settings, this is the first thing to check.</p>\n\n<Warning>\n    **`members:write` escalates past the key's own scopes.** An invited role is checked against the *owner's* workspace permissions, not against the calling key's scopes, so a key that holds nothing but `members:write` can invite an address its holder controls into any role the owner could grant. Two limits apply and only two: the owner system role is never assignable through an invite (`409` `role.owner_not_assignable`), and a role granting permissions the owner does not hold is refused with `403` `auth.permission_denied`. Do not put `members:write` on a CI key that only needs to read variables.\n</Warning>\n\n<Warning>\n    **`keys:write` can mint further keys, but cannot escalate.** A requested scope above the calling key's own aborts the whole request with `403` `auth.scope_ceiling_exceeded`, and whatever survives is then clamped silently to the owner's role, so a requested `write` can land as `read` or `none`. A fenced key must pass a non-empty `projectIds` subset of its own fence. Scope CI keys to `variables:read` and `versions:read` unless they genuinely need more.\n</Warning>\n",p=[],k={"api/overview":{title:"Overview"},"api/authentication":{title:"Authentication"},"api/encryption":{title:"Encrypting Values"},"api/errors":{title:"Errors"},"api/sdk":{title:"Official SDKs"},"api/endpoints/get-me":{title:"Get the calling identity"},"api/endpoints/create-workspace":{title:"Create a workspace"},"api/endpoints/delete-workspace":{title:"Delete the current workspace"},"api/endpoints/get-current-workspace":{title:"Get the current workspace"},"api/endpoints/list-workspaces":{title:"List your workspaces"},"api/endpoints/update-workspace":{title:"Update the current workspace"},"api/endpoints/claim-custom-domain":{title:"Claim a custom domain"},"api/endpoints/get-custom-domain":{title:"Get the custom domain"},"api/endpoints/remove-custom-domain":{title:"Remove the custom domain"},"api/endpoints/verify-custom-domain":{title:"Verify the custom domain"},"api/endpoints/delete-member":{title:"Remove a member"},"api/endpoints/invite-member":{title:"Invite a member"},"api/endpoints/leave-workspace":{title:"Leave the workspace"},"api/endpoints/list-members":{title:"List workspace members"},"api/endpoints/update-member":{title:"Change a member's role"},"api/endpoints/list-invites":{title:"List pending invites"},"api/endpoints/resend-invite":{title:"Resend an invite email"},"api/endpoints/revoke-invite":{title:"Revoke an invite"},"api/endpoints/create-role":{title:"Create a custom role"},"api/endpoints/delete-role":{title:"Delete a custom role"},"api/endpoints/get-role":{title:"Get a role"},"api/endpoints/list-permissions":{title:"List the permission catalogue"},"api/endpoints/list-roles":{title:"List roles"},"api/endpoints/update-role":{title:"Update a custom role"},"api/endpoints/create-product":{title:"Create a product"},"api/endpoints/delete-product":{title:"Delete a product"},"api/endpoints/duplicate-product":{title:"Duplicate a product"},"api/endpoints/get-product":{title:"Get a product"},"api/endpoints/list-product-projects":{title:"List a product's projects"},"api/endpoints/list-products":{title:"List products"},"api/endpoints/update-product":{title:"Update a product"},"api/endpoints/grant-product-access":{title:"Grant product access"},"api/endpoints/list-product-access":{title:"List product access grants"},"api/endpoints/revoke-product-access":{title:"Revoke product access"},"api/endpoints/create-project":{title:"Create a project"},"api/endpoints/delete-project":{title:"Delete a project"},"api/endpoints/duplicate-project":{title:"Duplicate a project"},"api/endpoints/get-project":{title:"Get a project"},"api/endpoints/list-projects":{title:"List projects"},"api/endpoints/update-project":{title:"Update a project"},"api/endpoints/create-environment":{title:"Create an environment"},"api/endpoints/delete-environment":{title:"Delete an environment"},"api/endpoints/duplicate-environment":{title:"Duplicate an environment"}
1,"api/endpoints/expose-environment":{title:"Expose or unexpose an environment"},"api/endpoints/get-environment":{title:"Get an environment"},"api/endpoints/list-environments":{title:"List a project's environments"},"api/endpoints/update-environment":{title:"Update an environment"},"api/endpoints/apply-variables":{title:"Apply a set of variable changes atomically"},"api/endpoints/create-variable":{title:"Create a variable"},"api/endpoints/create-variables":{title:"Create variables in bulk"},"api/endpoints/delete-variable-by-id":{title:"Delete a variable by id"},"api/endpoints/delete-variable-by-name":{title:"Delete a variable by name"},"api/endpoints/delete-variables":{title:"Delete variables by name"},"api/endpoints/get-variable":{title:"Get a variable by id"},"api/endpoints/get-variable-by-name":{title:"Get a variable by name"},"api/endpoints/get-variable-history":{title:"List a variable's history"},"api/endpoints/list-variables":{title:"List variables"},"api/endpoints/update-variable-by-id":{title:"Update a variable by id"},"api/endpoints/update-variable-by-name":{title:"Update a variable by name"},"api/endpoints/create-version":{title:"Create a version"},"api/endpoints/get-version":{title:"Get a version"},"api/endpoints/list-version-entries":{title:"List version entry ciphertext"},"api/endpoints/list-versions":{title:"List versions"},"api/endpoints/reencrypt-version-entries":{title:"Write re-encrypted version entry ciphertext"},"api/endpoints/rollback-version":{title:"Roll back to a version"},"api/endpoints/create-key":{title:"Create an API key"},"api/endpoints/delete-key":{title:"Revoke an API key"},"api/endpoints/get-key":{title:"Get an API key"},"api/endpoints/list-keys":{title:"List API keys"},"api/endpoints/update-key":{title:"Update an API key"},"api/endpoints/create-webhook":{title:"Create a webhook endpoint"},"api/endpoints/delete-webhook":{title:"Delete a webhook endpoint"},"api/endpoints/get-delivery":{title:"Get a webhook delivery"},"api/endpoints/get-webhook":{title:"Get a webhook endpoint"},"api/endpoints/list-deliveries":{title:"List webhook deliveries"},"api/endpoints/list-event-types":{title:"List webhook event types"},"api/endpoints/list-webhooks":{title:"List webhook endpoints"},"api/endpoints/redeliver-webhook":{title:"Redeliver a webhook delivery"},"api/endpoints/rotate-webhook-secret":{title:"Rotate a webhook signing secret"},"api/endpoints/update-webhook":{title:"Update a webhook endpoint"},"webhooks/overview":{title:"Webhooks Overview"},"webhooks/events":{title:"Event Types"},"webhooks/events/workspace/created":{title:"workspace.created"},"webhooks/events/workspace/updated":{title:"workspace.updated"},"webhooks/events/workspace/deleted":{title:"workspace.deleted"},"webhooks/events/product/created":{title:"product.created"},"webhooks/events/product/updated":{title:"product.updated"},"webhooks/events/product/deleted":{title:"product.deleted"},"webhooks/events/project/created":{title:"project.created"},"webhooks/events/project/updated":{title:"project.updated"},"webhooks/events/project/deleted":{title:"project.deleted"},"webhooks/events/environment/created":{title:"environment.created"},"webhooks/events/environment/updated":{title:"environment.updated"},"webhooks/events/environment/deleted":{title:"environment.deleted"},"webhooks/events/environment/exposed":{title:"environment.exposed"},"webhooks/events/environment/unexposed":{title:"environment.unexposed"},"webhooks/events/environment/example-enabled":{title:"environment.example_enabled"},"webhooks/events/environment/example-disabled":{title:"environment.example_disabled"},"webhooks/events/variable/created":{title:"variable.created"},"webhooks/events/variable/updated":{title:"variable.updated"},"webhooks/events/variable/deleted":{title:"variable.deleted"},"webhooks/events/variable/bulk_created":{title:"variable.bulk_created"},"webhooks/events/variable/bulk_deleted":{title:"variable.bulk_deleted"},"webhooks/events/variable/bulk_updated":{title:"variable.bulk_updated"},"webhooks/events/variable/applied":{title:"variable.applied"},"webhooks/events/version/created":{title:"variable_version.created"},"webhooks/events/version/rolled_back":{title:"variable_version.rolled_back"},"webhooks/events/member/invited":{title:"member.invited"},"webhooks/events/member/invite_resent":{title:"member.invite_resent"}
1,"webhooks/events/member/invite_revoked":{title:"member.invite_revoked"},"webhooks/events/member/accepted":{title:"member.accepted"},"webhooks/events/member/updated":{title:"member.updated"},"webhooks/events/member/removed":{title:"member.removed"},"webhooks/events/key/created":{title:"key.created"},"webhooks/events/key/updated":{title:"key.updated"},"webhooks/events/key/rotated":{title:"key.rotated"},"webhooks/events/key/deleted":{title:"key.deleted"},"webhooks/events/cli_token/created":{title:"cli_token.created"},"webhooks/events/cli_token/revoked":{title:"cli_token.revoked"},"webhooks/events/role/created":{title:"role.created"},"webhooks/events/role/updated":{title:"role.updated"},"webhooks/events/role/deleted":{title:"role.deleted"},"webhooks/events/subscription/created":{title:"subscription.created"},"webhooks/events/subscription/updated":{title:"subscription.updated"},"webhooks/events/webhook/created":{title:"webhook.created"},"webhooks/events/webhook/updated":{title:"webhook.updated"},"webhooks/events/webhook/deleted":{title:"webhook.deleted"},"webhooks/events/webhook/secret_rotated":{title:"webhook.secret_rotated"},"webhooks/events/webhook/delivery_failed":{title:"webhook.delivery_failed"},"webhooks/signing":{title:"Signature Verification"},"webhooks/managing":{title:"Managing Webhooks"},"api/changelog":{title:"API Changelog"}},d=[{depth:2,slug:"the-key-is-the-whole-check",text:"The key is the whole check"},{depth:2,slug:"what-a-key-grants",text:"What a key grants"},{depth:2,slug:"examples",text:"Examples"},{depth:2,slug:"failure-modes",text:"Failure modes"},{depth:2,slug:"last-used",text:"Last used"},{depth:2,slug:"keys-change-underneath-you",text:"Keys change underneath you"}];function u(){return d}function o(t){const s={a:"a",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",span:"span",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...t.components},{CodeGroup:a,Note:n,Warning:i}=s;return a||r("CodeGroup"),n||r("Note"),i||r("Warning"),e.jsxs(e.Fragment,{children:[e.jsxs("p",{children:["Every request must include an ",e.jsx(s.code,{children:"Authorization"})," header carrying a personal access key (",e.jsx(s.code,{children:"ev_sk_..."}),")."]}),`
2`,e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsx(s.code,{children:e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"Authorization:"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" Bearer"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" ev_sk_xxxxxxxxxxxx"})]})})}),`
3`,e.jsxs("p",{children:["The ",e.jsx(s.code,{children:"Bearer "})," prefix is stripped when present and is otherwise optional, so a bare ",e.jsx(s.code,{children:"Authorization: ev_sk_..."})," is accepted too. What is not optional is the ",e.jsx(s.code,{children:"ev_sk_"})," prefix: a value that does not start with it is rejected before any database lookup happens."]}),`
4`,e.jsxs("p",{children:["Generate keys at ",e.jsx(s.a,{href:"/dashboard/keys",children:"envless.cloud/dashboard/keys"}),". The full key is shown once. Copy it into your secret store immediately. See ",e.jsx(s.a,{href:"/docs/auth/keys",children:"Keys"})," for naming, rotation, and revocation."]}),`
5`,e.jsx(n,{children:e.jsxs(s.p,{children:["A CLI session token (",e.jsx(s.code,{children:"env_cli_..."}),") is a different credential and is refused on these endpoints. The two families never overlap: the REST API rejects anything that is not ",e.jsx(s.code,{children:"ev_sk_"}),", and the dashboard and ",e.jsx(s.code,{children:"/cli/*"})," endpoints reject an ",e.jsx(s.code,{children:"ev_sk_"})," key with ",e.jsx(s.code,{children:"auth.api_key_not_accepted"}),"."]})}),`
6`,e.jsxs("p",{children:["The same routes are served twice, once at the root and once under ",e.jsx(s.co
6de,{children:"/v1"}),", so ",e.jsx(s.code,{children:"https://api.envless.cloud/workspace"})," and ",e.jsx(s.code,{children:"https://api.envless.cloud/v1/workspace"})," are the same endpoint."]}),`
7`,e.jsx(s.h2,{id:"the-key-is-the-whole-check",children:"The key is the whole check"}),`
8`,e.jsx(i,{children:e.jsxs(s.p,{children:[e.jsx(s.strong,{children:"A key never presents a code, of either kind."})," Neither the ",e.jsx(s.a,{href:"/docs/auth/two-factor",children:"two factor"})," sign-in challenge nor ",e.jsx(s.a,{href:"/docs/auth/step-up",children:"step-up verification"})," is ever put to this surface. A key that has the scope performs the operation, including the ones the dashboard guards: deleting another key or rewriting its scopes, removing a member, deleting a project, rolling a version back, creating a webhook endpoint and rotating its signing secret. The owner turning two factor on for their own account adds no challenge here and revokes nothing."]})}),`
9`,e.jsx(i,{children:e.jsxs(s.p,{children:[e.jsx(s.strong,{children:"What can stop a live key is a workspace that requires two factor."})," A workspace owner or admin can require the second factor of every member. While that requirement is on, a key whose ",e.jsx(s.strong,{children:"owner"})," has not enabled two factor on their account is refused on every route, in the auth middleware, before scopes are read: ",e.jsx(s.code,{children:"403"})," with ",e.jsx(s.code,{children:"auth.workspace_two_factor_required"}),". That is enforceable on a credential that cannot answer a challenge because the question is asked of the owner's account record rather than of the request. Nothing is deleted, deactivated or re-scoped, and the key answers normally again the moment its owner enrolls or the requirement is turned off. See ",e.jsx(s.a,{href:"/docs/auth/two-factor#requiring-it-across-a-workspace",children:"Requiring it across a workspace"}),"."]})}),`
10`,e.jsxs("p",{children:["That is structural rather than an omission. Both of those checks are answered by a human at a keyboard: one reads a code out of an inbox, the other reads it off a phone. An ",e.jsx(s.code,{children:"ev_sk_"})," key is a pre-authorized machine credential running unattended in CI, so there is nobody to ask, and gating the surface would break every consumer that uses it. The gates sit on ",e.jsx(s.strong,{children:"minting"})," instead: creating a key in the dashboard, rotating one, deleting one, and changing its scopes or allowlist all require step-up, and the account that does the minting can be second-factor protected. The workspace requirement above is the one gate that reaches a key already in use, and it manages that by asking a question about the owner's account rather than about the request."]}),`
11`,e.jsx("p",{children:"What follows from that is the control model. A key's blast radius is its scopes and its project allowlist, nothing else, and the only revocation is deletion or rotation from the dashboard. Scope keys to the minimum, keep one key per consumer so a single revocation is targeted, and treat a leaked key as fully live until the moment somebody deletes it."}),`
12`,e.jsx(s.h2,{id:"what-a-key-grants",children:"What a key grants"}),`
13`,e.jsxs("p",{children:["Every key belongs to a single workspace and carries a ",e.jsx(s.strong,{children:"scope per resource"}),". Each resource is granted one of three levels (",e.jsx(s.code,{children:"none"}),", ",e.jsx(s.code,{children:"read"}),", or ",e.jsx(s.code,{children:"write"}),"), and ",e.jsx(s.code,{children:"write"})," implies ",e.jsx(s.code,{children:"read"}),". There are eleven resources:"]}),`
14`,e.jsxs(s.table,{children:[e.jsx(s.thead,{children:e.jsxs(s.tr,{children:[e.jsx(s.th,{children:"Resource"}),e.jsx(s.th,{children:e.jsx(s.code,{children:"read"})}),e.jsx(s.th,{children:e.jsx(s.code,{children:"write"})})]})}),e.jsxs(s.tbody,{children:[e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"workspace"})}),e.jsxs(s.td,{children:[e.jsx(s.code,{children:"GET /workspace"}),", ",e.jsx(s.code,{children:"GET /workspaces"}),", ",e.jsx(s.code,{children:"GET /workspace/domain"})]}),e.jsx(s.td,{children:"update / delete workspace, create workspace, claim / verify / remove custom domain"})]}
14),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"products"})}),e.jsx(s.td,{children:"list / get products, list a product's projects"}),e.jsx(s.td,{children:"create / update / delete / duplicate"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"product-access"})}),e.jsx(s.td,{children:"list a product's access grants"}),e.jsx(s.td,{children:"grant / revoke member access"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"projects"})}),e.jsx(s.td,{children:"list / get"}),e.jsx(s.td,{children:"create / update / delete / duplicate"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"environments"})}),e.jsx(s.td,{children:"list / get"}),e.jsx(s.td,{children:"create / update / delete / duplicate / expose"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"variables"})}),e.jsx(s.td,{children:"list / get (ciphertext), history"}),e.jsx(s.td,{children:"create / update / delete (single, bulk, apply)"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"versions"})}),e.jsx(s.td,{children:"list / get, list entries"}),e.jsx(s.td,{children:"create / rollback / re-encrypt entries"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"members"})}),e.jsx(s.td,{children:"list members, list invites"}),e.jsx(s.td,{children:"invite / update role / remove / leave, resend / revoke invite"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"roles"})}),e.jsx(s.td,{children:"list / get roles, list permissions"}),e.jsx(s.td,{children:"create / update / delete roles"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"keys"})}),e.jsx(s.td,{children:"list / get"}),e.jsx(s.td,{children:"create / update / delete"})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"webhooks"})}),e.jsx(s.td,{children:"list / get, event types, deliveries"}),e.jsx(s.td,{children:"create / update / delete, rotate secret, redeliver"})]})]})]}),`
15`,e.jsxs("p",{children:["A resource left at ",e.jsx(s.code,{children:"none"})," returns ",e.jsx(s.code,{children:"403"})," on every route that names it. Some routes require two scopes at once, and both are checked before the handler runs: every duplicate route also needs ",e.jsx(s.code,{children:"variables:write"})," (a product duplicate additionally needs ",e.jsx(s.code,{children:"projects:write"}),"), version rollback and entry re-encryption need ",e.jsx(s.code,{children:"variables:write"}),", reading a version or its entries needs ",e.jsx(s.code,{children:"variables:read"}),", and ",e.jsx(s.code,{children:"GET /products/:slug/projects"})," needs ",e.jsx(s.code,{children:"projects:read"}),"."]}),`
16`,e.jsxs("p",{children:["A key can additionally be pinned to an ",e.jsx(s.strong,{children:"allowlist of projects"}),". Leave it empty for the whole workspace, or list specific project IDs (not slugs) to fence the key in."]}),`
17`,e.jsxs("p",{children:["What the fence narrows is ",e.jsx(s.code,{children:"environments"}),", ",e.jsx(s.code,{children:"variables"}),", ",e.jsx(s.code,{children:"versions"})," and the project reads: ",e.jsx(s.code,{children:"GET /projects"})," and ",e.jsx(s.code,{children:"GET /products/{slug}/projects"})," return only the fenced projects, and any route resolving a project outside the fence is ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.key_project_restricted"}),"."]}),`
18`,e.jsxs("p",{children:["The workspace-wide surfaces are not narrowed, they are closed. Each of these refuses a fenced key outright with ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.key_project_restricted"}),", whatever scope the key holds:"]}),`
19`,e.jsxs(s.table,{children:[e.jsx(s.thead,{children:e.jsxs(s.tr,{children:[e.jsx(s.th,{children:"Refused for a fenced key"}),e.jsx(s.th,{children:"Why"})]})}),e.jsxs(s.tbody,{children:[e.jsxs(s.tr,{children:[e.jsxs(s.td,{children:[e.jsx(s.code,{children:"GET /keys"}),", ",e.jsx(s.code,{children:"GET /keys/{id}"}),", ",e.jsx(s.code,{children:"PATCH /keys/{id}"}
19),", ",e.jsx(s.code,{children:"DELETE /keys/{id}"})]}),e.jsx(s.td,{children:"Keys are workspace-wide, so there is no project subset of the key list to hand back"})]}),e.jsxs(s.tr,{children:[e.jsxs(s.td,{children:["Every webhook route, including the reads and ",e.jsx(s.code,{children:"GET /webhooks/event-types"})]}),e.jsx(s.td,{children:"Endpoints subscribe to workspace-wide events and have no project dimension"})]}),e.jsxs(s.tr,{children:[e.jsxs(s.td,{children:[e.jsx(s.code,{children:"POST /products"}),", ",e.jsx(s.code,{children:"PATCH"})," and ",e.jsx(s.code,{children:"DELETE /products/{slug}"}),", ",e.jsx(s.code,{children:"POST /products/{slug}/duplicate"}),", and ",e.jsx(s.code,{children:"PUT"})," and ",e.jsx(s.code,{children:"DELETE /products/{slug}/access/{memberId}"})]}),e.jsx(s.td,{children:"A product spans projects the fence may not cover"})]}),e.jsxs(s.tr,{children:[e.jsxs(s.td,{children:[e.jsx(s.code,{children:"POST /projects"})," and ",e.jsx(s.code,{children:"POST /projects/{slug}/duplicate"})]}),e.jsx(s.td,{children:"The new project could not be covered by the allowlist"})]})]})]}),`
20`,e.jsxs("p",{children:[e.jsx(s.code,{children:"POST /keys"})," is the one key route a fenced key keeps. There ",e.jsx(s.code,{children:"projectIds"})," becomes mandatory and must be a non-empty subset of the caller's own fence, so a fenced key can mint narrower keys but can never read back the list it just added to."]}),`
21`,e.jsxs("p",{children:["Everything else at workspace level is untouched by the fence, because none of it has a project dimension to narrow: ",e.jsx(s.code,{children:"GET /me"}),", the workspace and domain routes, ",e.jsx(s.code,{children:"GET /workspaces"}),", all of ",e.jsx(s.code,{children:"members"}),", ",e.jsx(s.code,{children:"invites"})," and ",e.jsx(s.code,{children:"roles"}),", and the product reads ",e.jsx(s.code,{children:"GET /products"}),", ",e.jsx(s.code,{children:"GET /products/{slug}"})," and ",e.jsx(s.code,{children:"GET /products/{slug}/access"}),"."]}),`
22`,e.jsx(n,{children:e.jsxs(s.p,{children:["Scope is checked in middleware and the fence inside the handler, so the two errors are ordered. A fenced key without ",e.jsx(s.code,{children:"keys:read"})," gets ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.scope_missing"})," on ",e.jsx(s.code,{children:"GET /keys"}),"; add the scope and the same key gets ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.key_project_restricted"}),". Granting the scope never opens a closed route."]})}),`
23`,e.jsxs("p",{children:["Grant the narrowest scopes that get the job done. A CI key that only pulls ciphertext needs nothing beyond ",e.jsx(s.code,{children:"variables:read"}),": the variables routes take the project and environment slugs in the path and resolve them internally, so no ",e.jsx(s.code,{children:"projects"})," or ",e.jsx(s.code,{children:"environments"})," scope is involved. Add ",e.jsx(s.code,{children:"workspace:read"})," only if the client has to resolve the workspace ID itself. ",e.jsx(s.code,{children:"@goenvless/env/edge"})," is the common case: its ",e.jsx(s.code,{children:"load({ token, project, environment, passphrase })"})," calls ",e.jsx(s.code,{children:"GET /workspace"})," first whenever no ",e.jsx(s.code,{children:"workspaceId"})," and no ",e.jsx(s.code,{children:"key"})," were passed, because the passphrase is derived against the workspace ID. Pass ",e.jsx(s.code,{children:"workspaceId"})," or a raw ",e.jsx(s.code,{children:"key"})," and that call disappears, leaving ",e.jsx(s.code,{children:"variables:read"})," as the only scope the edge loader needs."]}),`
24`,e.jsx(n,{children:e.jsxs(s.p,{children:["A key created without a ",e.jsx(s.code,{children:"scopes"})," object gets ",e.jsxs(s.strong,{children:["every scope set to ",e.jsx(s.code,{children:"none"})]}),". It can authenticate but not touch any resource until you assign scopes. Set them when you create the key."]})}),`
25`,e.jsx(s.h2,{id:"examples",children:"Examples"}),`
26`,e.jsxs("p",{children:[e.jsx(s.code,{children:"GET /me"})," is the only endpoint that requires no scope at all, which makes it the way to prove a key is live before you debug anything else. It returns the owning user, the bound ",e.jsx(s.co
26de,{children:"workspaceId"})," and the key's own ",e.jsx(s.code,{children:"id"}),", ",e.jsx(s.code,{children:"name"})," and ",e.jsx(s.code,{children:"createdAt"}),"."]}),`
27`,e.jsxs(a,{children:[e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash","data-title":"curl",children:e.jsxs(s.code,{children:[e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"curl"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" https://api.envless.cloud/me"}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#FF7B72"},children:" \\"})]}),`
28`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"    -H"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:' "Authorization: Bearer ev_sk_xxxxxxxxxxxx"'})]})]})}),e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"ts","data-lang":"ts","data-title":"fetch",children:e.jsxs(s.code,{children:[e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"const"}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" res"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" ="}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" await"}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:" fetch"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"'https://api.envless.cloud/me'"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:", {"})]}),`
29`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"    headers: { Authorization: "}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"`Bearer ${"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"process"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"."}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"env"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"."}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"ENVLESS_TOKEN"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"}`"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" },"})]}),`
30`,e.jsx(s.span,{className:"line",children:e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"});"})}),`
31`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"const"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" { "}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"data"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" } "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"="}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" await"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" res."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"json"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"}
31,children:"();"})]})]})}),e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"python","data-lang":"python","data-title":"python",children:e.jsxs(s.code,{children:[e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"import"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" os, requests"})]}),`
32`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"res "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"="}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" requests.get("})]}),`
33`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'    "https://api.envless.cloud/me"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:","})]}),`
34`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#E36209","--shiki-dark":"#FFA657"},children:"    headers"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"="}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"{"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"Authorization"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:": "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"f"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"Bearer '}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#FF7B72"},children:"{"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"os.environ["}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"'ENVLESS_TOKEN'"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"]"}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#FF7B72"},children:"}"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"},"})]}),`
35`,e.jsx(s.span,{className:"line",children:e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:")"})}),`
36`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"data "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"="}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" res.json()["}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"data"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"]"})]})]})}),e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"ruby","data-lang":"ruby","data-title":"ruby",children:e.jsxs(s.code,{children:[e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"require"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:' "net/http"'})]}),`
37`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"require"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:' "json"'})]}),`
38`,e.jsx(s.span,{className:"line"}),`
39`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#E36209","--shiki-dark":"#FFA657"},children:"uri"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" ="}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" URI"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"https://api.envless.cloud/me"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:")"})]}),`
40`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#E36209","--shiki-dark":"#FFA657"},children:"headers"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" ="}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" {"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"Authorization"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" => "}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"Bearer '}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#FF7B72"},children:"#{"}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"ENV"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"fetch"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"ENVLESS_TOKEN"'}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:")"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#FF7B72"},children:"}"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"}"})]}),`
41`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#E36209","--shiki-dark":"#FFA657"},children:"data"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" ="}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" JSON"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"parse"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"Net"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"::"}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"HTTP"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"get_response"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"(uri, headers)."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"body"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:")["}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"data"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"]"})]})]})}
41),e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"php","data-lang":"php","data-title":"php",children:e.jsxs(s.code,{children:[e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"$context "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"="}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" stream_context_create"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"(["}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"'http'"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" =>"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" ["})]}),`
42`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"    'header'"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" =>"}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" 'Authorization: Bearer '"}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" ."}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" getenv"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"'ENVLESS_TOKEN'"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"),"})]}),`
43`,e.jsx(s.span,{className:"line",children:e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"]]);"})}),`
44`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"$data "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"="}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" json_decode"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"file_get_contents"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"'https://api.envless.cloud/me'"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:", "}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"false"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:", $context), "}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"true"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:")["}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:"'data'"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"];"})]})]})}),e.jsx(s.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"go","data-lang":"go","data-title":"go",children:e.jsxs(s.code,{children:[e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"req, _ "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:":="}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" http."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"NewRequest"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"GET"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:", "}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"https://api.envless.cloud/me"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:", "}),e.jsx(s.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"nil"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:")"})]}),`
45`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"req.Header."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"Set"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"Authorization"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:", "}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"Bearer "'}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" +"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" os."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"Getenv"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"("}),e.jsx(s.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:'"ENVLESS_TOKEN"'}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"))"})]}),`
46`,e.jsxs(s.span,{className:"line",children:[e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"res, _ "}),e.jsx(s.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:":="}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:" http.DefaultClient."}),e.jsx(s.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#D2A8FF"},children:"Do"}),e.jsx(s.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"(req)"})]})]})})]}),`
47`,e.jsxs("p",{children:["The ",e.jsx(s.a,{href:"/docs/sdk/overview",children:"TypeScript"}),", ",e.jsx(s.a,{href:"/docs/python/overview",children:"Python"}),", ",e.jsx(s.a,{href:"/docs/ruby/overview",children:"Ruby"})," and ",e.jsx(s.a,{href:"/docs/php/overview",children:"PHP"})," SDKs send the header for you, and this whole call is ",e.jsx(s.co
47de,{children:"envless.me.get()"})," in the first two, ",e.jsx(s.code,{children:"Envless.me.get"})," in Ruby and ",e.jsx(s.code,{children:"$envless->me->get()"})," in PHP."]}),`
48`,e.jsxs("p",{children:["Every response is the same envelope: ",e.jsx(s.code,{children:"success"}),", ",e.jsx(s.code,{children:"code"}),", ",e.jsx(s.code,{children:"message"}),", ",e.jsx(s.code,{children:"error"}),", ",e.jsx(s.code,{children:"data"})," and ",e.jsx(s.code,{children:"_req"}),". On a failure ",e.jsx(s.code,{children:"error.code"})," carries the machine-readable string from the tables below, and ",e.jsx(s.code,{children:"data"})," is ",e.jsx(s.code,{children:"null"}),"."]}),`
49`,e.jsx(s.h2,{id:"failure-modes",children:"Failure modes"}),`
50`,e.jsxs(s.table,{children:[e.jsx(s.thead,{children:e.jsxs(s.tr,{children:[e.jsx(s.th,{children:"Status"}),e.jsx(s.th,{children:e.jsx(s.code,{children:"error.code"})}),e.jsx(s.th,{children:"Meaning"}),e.jsx(s.th,{children:"Fix"})]})}),e.jsxs(s.tbody,{children:[e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"401"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.api_key_required"})}),e.jsxs(s.td,{children:["No ",e.jsx(s.code,{children:"Authorization"})," header, or the value does not start with ",e.jsx(s.code,{children:"ev_sk_"}),"."]}),e.jsx(s.td,{children:"Add or fix the header."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"401"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.api_key_not_found"})}),e.jsx(s.td,{children:"Key was deleted, rotated, deactivated, or never existed."}),e.jsx(s.td,{children:"Generate a new one and update your secret store."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"401"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.api_key_user_missing"})}),e.jsx(s.td,{children:"The key is valid but its owner account no longer exists."}),e.jsx(s.td,{children:"Create the key again under a live account."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"401"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.api_key_owner_not_member"})}),e.jsx(s.td,{children:"The key's owner has left or been removed from the workspace."}),e.jsx(s.td,{children:"Re-add the owner, or issue the key from a current member."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"403"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.workspace_two_factor_required"})}),e.jsx(s.td,{children:"The key's workspace requires two factor authentication and the key's owner has not enabled it on their account. Refused before scopes are read, on every route."}),e.jsxs(s.td,{children:["The owner turns two factor on from ",e.jsx(s.a,{href:"/dashboard/account",children:"Account settings"}),", or someone with ",e.jsx(s.code,{children:"workspace:update"})," turns the requirement off."]})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"403"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.scope_missing"})}),e.jsxs(s.td,{children:["The key is valid but lacks the scope the endpoint needs. The message and ",e.jsx(s.code,{children:"error.resource"})," name it as ",e.jsx(s.code,{children:"resource:level"}),"."]}),e.jsx(s.td,{children:"Add the scope to the key in the dashboard, or use a key that has it."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"403"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.key_project_restricted"})}),e.jsxs(s.td,{children:["The key is fenced to specific projects and the request touched something outside them, or touched a closed workspace-wide surface: any key route other than ",e.jsx(s.code,{children:"POST /keys"}),", any webhook route, a product or product-access write, or project creation and duplication."]}),e.jsxs(s.td,{children:["Use a key with no project fence. On ",e.jsx(s.code,{children:"POST /keys"}),", send a non-empty ",e.jsx(s.code,{children:"projectIds"})," subset of the caller's own fence."]})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"403"})}
50),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.product_restricted"})}),e.jsx(s.td,{children:"The key owner has no grant on the private product the target project belongs to."}),e.jsx(s.td,{children:"Grant the owner access to the product."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"403"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"auth.scope_ceiling_exceeded"})}),e.jsxs(s.td,{children:[e.jsx(s.code,{children:"POST /keys"})," asked for a ",e.jsx(s.code,{children:"resource:level"})," above what the calling key holds. Nothing is created."]}),e.jsx(s.td,{children:"Request scopes at or below the calling key's own."})]}),e.jsxs(s.tr,{children:[e.jsx(s.td,{children:e.jsx(s.code,{children:"402"})}),e.jsx(s.td,{children:e.jsx(s.code,{children:"plan.upgrade_required"})}),e.jsx(s.td,{children:"The target project is locked because it is over the plan's project limit."}),e.jsx(s.td,{children:"Upgrade the plan, or delete projects until the target is unlocked."})]})]})]}),`
51`,e.jsx(i,{children:e.jsx(s.p,{children:"Never put a key in client-side code, public repos, build logs, or chat. Scope every key to the minimum it needs: a leaked key can do exactly what its scopes allow until you revoke it, so a read-only key limits the blast radius."})}),`
52`,e.jsx(s.h2,{id:"last-used",children:"Last used"}),`
53`,e.jsxs("p",{children:["Each key records a single ",e.jsx(s.code,{children:"lastUsedAt"})," timestamp, shown in the dashboard, and nothing else. The write is throttled: a key that was already touched within the last 60 seconds is not written again, and the update is fire-and-forget so a failed write never fails the request. There is no per-key request counter and no per-request log to read back, so treat ",e.jsx(s.code,{children:"lastUsedAt"})," as a liveness signal for finding dormant keys to rotate, not as usage accounting."]}),`
54`,e.jsx(s.h2,{id:"keys-change-underneath-you",children:"Keys change underneath you"}),`
55`,e.jsxs("p",{children:["Two things change what a key can do without anyone touching the key. A plan change is not one of them: keys work on every plan, Free included, so a downgrade never deactivates a key, though a project the downgrade locks still answers ",e.jsx(s.code,{children:"402"})," to it (see ",e.jsx(s.a,{href:"/docs/manage/plans-and-billing",children:"Plans and billing"}),")."]}),`
56`,e.jsxs("p",{children:[e.jsx(s.strong,{children:"A role change re-clamps the scopes of every key its owner holds in that workspace."})," Scopes are clamped to the owner's workspace permissions at creation time and again whenever the owner's role, or that role's permission set, changes. A key can silently drop from ",e.jsx(s.code,{children:"write"})," to ",e.jsx(s.code,{children:"read"}),", or to ",e.jsx(s.code,{children:"none"}),", so read ",e.jsx(s.code,{children:"scopes"})," back rather than assuming the key still holds what you asked for."]}),`
57`,e.jsxs("p",{children:[e.jsx(s.strong,{children:"A workspace two factor requirement stops every key whose owner has not enrolled."})," Turning the requirement on is an admin action taken in the dashboard, and it lands on the keys immediately: every request returns ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.workspace_two_factor_required"})," until that owner enables two factor or the requirement comes back off. The key's record is not changed, so nothing in ",e.jsx(s.code,{children:"GET /keys"})," looks different. If a pipeline stops the same day someone tightened workspace settings, this is the first thing to check."]}),`
58`,e.jsx(i,{children:e.jsxs(s.p,{children:[e.jsxs(s.strong,{children:[e.jsx(s.code,{children:"members:write"})," escalates past the key's own scopes."]})," An invited role is checked against the ",e.jsx(s.em,{children:"owner's"})," workspace permissions, not against the calling key's scopes, so a key that holds nothing but ",e.jsx(s.code,{children:"members:write"})," can invite an address its holder controls into any role the owner could grant. Two limits apply and only two: the owner system role is never assignable through an invite (",e.jsx(s.co
58de,{children:"409"})," ",e.jsx(s.code,{children:"role.owner_not_assignable"}),"), and a role granting permissions the owner does not hold is refused with ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.permission_denied"}),". Do not put ",e.jsx(s.code,{children:"members:write"})," on a CI key that only needs to read variables."]})}),`
59`,e.jsx(i,{children:e.jsxs(s.p,{children:[e.jsxs(s.strong,{children:[e.jsx(s.code,{children:"keys:write"})," can mint further keys, but cannot escalate."]})," A requested scope above the calling key's own aborts the whole request with ",e.jsx(s.code,{children:"403"})," ",e.jsx(s.code,{children:"auth.scope_ceiling_exceeded"}),", and whatever survives is then clamped silently to the owner's role, so a requested ",e.jsx(s.code,{children:"write"})," can land as ",e.jsx(s.code,{children:"read"})," or ",e.jsx(s.code,{children:"none"}),". A fenced key must pass a non-empty ",e.jsx(s.code,{children:"projectIds"})," subset of its own fence. Scope CI keys to ",e.jsx(s.code,{children:"variables:read"})," and ",e.jsx(s.code,{children:"versions:read"})," unless they genuinely need more."]})})]})}function j(t={}){const{wrapper:s}=t.components||{};return s?e.jsx(s,{...t,children:e.jsx(o,{...t})}):o(t)}function r(t,s){throw new Error("Expected component `"+t+"` to be defined: you likely forgot to import, pass, or provide it.")}export{c as body,j as default,l as frontmatter,u as getHeadings,d as headings,k as navTitles,p as updates};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.