From 4823eeccf3dc6bf665e45a206426685f79cf5adc Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 10:27:49 +0200 Subject: [PATCH 1/8] build: vendor the management OpenAPI specs and a generator for them The four specs (environment, workspace, platform, account) come from cbox-id integration/wave-6. scripts/generate_management.py turns them into typed modules; --check fails when the output is stale and --fetch refreshes a spec from a running server. typing-extensions becomes a runtime dependency for NotRequired/TypedDict on 3.10; pyyaml is dev-only. --- openapi/account.yaml | 719 ++ openapi/environment.yaml | 16936 +++++++++++++++++++++++++++++++ openapi/platform.yaml | 953 ++ openapi/workspace.yaml | 1852 ++++ pyproject.toml | 23 +- scripts/__init__.py | 0 scripts/generate_management.py | 1205 +++ 7 files changed, 21686 insertions(+), 2 deletions(-) create mode 100644 openapi/account.yaml create mode 100644 openapi/environment.yaml create mode 100644 openapi/platform.yaml create mode 100644 openapi/workspace.yaml create mode 100644 scripts/__init__.py create mode 100644 scripts/generate_management.py diff --git a/openapi/account.yaml b/openapi/account.yaml new file mode 100644 index 0000000..6dfff99 --- /dev/null +++ b/openapi/account.yaml @@ -0,0 +1,719 @@ +# GENERATED by `php artisan openapi:build` from resources/openapi/base/account.yaml +# and the action registry (app/Actions). Edit the base or the action, never this file. +openapi: 3.1.0 +info: + title: 'Cbox ID — My Account API' + version: 1.0.0 + description: | + **Your own account** — the person calling, and nobody else: your profile, where you are + signed in, which applications may act as you, your own API keys for the apps built on + this environment, your sign-in methods, your trusted devices and the organizations you + belong to. The same actions the + console's My account pages run, with the same rules, refusals and audit trail. + + **Who may call it.** Only you, through an access token you delegated carrying the + `account:*` scope each operation names. **No management key is accepted** — no key + acts as a person. There is no account id in any path: every operation acts on the + person behind the token, and an id of somebody else's session, passkey, key or device + is simply not found. + + **What is not here, on purpose.** Changing your password, turning on a second factor, + new recovery codes and registering a passkey are ceremonies you perform in a browser — + a token that could do them could take the account over. + + Served on the host of the environment your account belongs to: an environment's own + host for its own people, with a token that environment issued; the platform root's + host for a workspace's team and the operators, with the token the root issued them + (`cbox login` at the root). + + **Envelope.** Every success returns `{ "data": … }` (or `204` with no body). Every + error returns `{ "error": "", "message": "" }`. +servers: + - + url: 'https://{host}/api/v1' + description: "Your environment's own host" + variables: + host: + default: acme.cboxid.com +security: + - + PersonToken: [] +tags: + - name: Profile + - name: Sessions + - name: Applications + description: 'The applications you let act as you.' + - name: 'API keys' + description: 'Your own keys for the apps built on this environment.' + - name: 'Sign-in methods' + - name: Devices + description: 'Your trusted devices, which approve sign-ins and agent requests.' + - name: Approvals + description: "Where a held operation's approval stands." + - name: Organizations + description: 'The organizations you are a member of — and leaving one.' +paths: + '/me/action-approvals/{id}': + get: + tags: + - Approvals + summary: 'Where an approval you asked for stands' + description: | + The `poll_url` of a `202 approval_required`: an approval this token raised, and only those — another token's id is a 404. `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + parameters: + - + name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + required: + - id + - status + properties: + id: + type: string + status: + type: string + enum: + - pending + - approved + - denied + - expired + - consumed + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/applications/{client_id}': + delete: + tags: + - Applications + summary: "Withdraw an application's access to your account; it must ask you again to act as you." + description: 'Requires scope `account:applications:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive.' + operationId: account_applications_revoke + x-action: account.applications.revoke + x-scope: 'account:applications:write' + x-danger: destructive + parameters: + - + name: client_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/devices/{device_id}': + delete: + tags: + - Devices + summary: 'Remove one of your trusted devices; it is no longer asked to approve anything.' + description: 'Requires scope `account:devices:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive.' + operationId: account_devices_remove + x-action: account.devices.remove + x-scope: 'account:devices:write' + x-danger: destructive + parameters: + - + name: device_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/organizations/{organization_id}/api-keys': + post: + tags: + - 'API keys' + summary: "Create an API key of your own for one of this environment's apps, in an organization you belong to. The value is returned once, as `token`." + description: 'Requires scope `account:api_keys:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical.' + operationId: account_api_keys_create + x-action: account.api_keys.create + x-scope: 'account:api_keys:write' + x-danger: critical + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + client_id: + type: string + description: 'The app this key is for.' + maxLength: 255 + name: + type: string + maxLength: 120 + permissions: + type: array + description: 'What the key may do in the app — each one a permission you hold there. Empty: it only identifies you.' + maxItems: 200 + items: + type: string + minLength: 1 + maxLength: 255 + expires_at: + type: + - string + - 'null' + description: 'When it stops working. Left out, it does not expire.' + format: date-time + required: + - client_id + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PersonalApiKey' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/organizations/{organization_id}/api-keys/{key_id}': + delete: + tags: + - 'API keys' + summary: 'Revoke one of your own API keys.' + description: 'Requires scope `account:api_keys:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive.' + operationId: account_api_keys_revoke + x-action: account.api_keys.revoke + x-scope: 'account:api_keys:write' + x-danger: destructive + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + - + name: key_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/organizations/{organization_id}/leave': + post: + tags: + - Organizations + summary: 'Leave one of your own organizations. The last owner cannot leave — transfer ownership first.' + description: 'Requires scope `account:organizations:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive.' + operationId: account_organizations_leave + x-action: account.organizations.leave + x-scope: 'account:organizations:write' + x-danger: destructive + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization you are leaving — one you are a member of.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/passkeys/{passkey_id}': + delete: + tags: + - 'Sign-in methods' + summary: 'Remove one of your passkeys.' + description: 'Requires scope `account:sign_in:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical.' + operationId: account_passkeys_remove + x-action: account.passkeys.remove + x-scope: 'account:sign_in:write' + x-danger: critical + parameters: + - + name: passkey_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /me/profile: + patch: + tags: + - Profile + summary: 'Change the display name on your own account.' + description: 'Requires scope `account:profile:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: write.' + operationId: account_profile_update + x-action: account.profile.update + x-scope: 'account:profile:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: 'Your display name.' + minLength: 1 + maxLength: 120 + required: + - name + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Profile' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /me/sessions/revoke-others: + post: + tags: + - Sessions + summary: 'Sign out every one of your sessions except the one you are using.' + description: 'Requires scope `account:sessions:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical.' + operationId: account_sessions_revoke_others + x-action: account.sessions.revoke_others + x-scope: 'account:sessions:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SessionsRevoked' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/sessions/{session_id}': + delete: + tags: + - Sessions + summary: 'Sign out one of your own sessions.' + description: 'Requires scope `account:sessions:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive.' + operationId: account_sessions_revoke + x-action: account.sessions.revoke + x-scope: 'account:sessions:write' + x-danger: destructive + parameters: + - + name: session_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/me/social/{provider}': + delete: + tags: + - 'Sign-in methods' + summary: 'Disconnect a social account from yours — never the last way you can sign in.' + description: 'Requires scope `account:sign_in:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical.' + operationId: account_social_unlink + x-action: account.social.unlink + x-scope: 'account:sign_in:write' + x-danger: critical + parameters: + - + name: provider + in: path + required: true + schema: + type: string + description: "The provider's key, for example `google`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' +components: + securitySchemes: + PersonToken: + type: http + scheme: bearer + description: 'An access token you delegated, carrying `account:*` scopes. Management keys are refused.' + parameters: + CboxApproval: + name: Cbox-Approval + in: header + required: false + description: 'The id of an approval this request was held for, once you approved it. Spent by this request, once.' + schema: + type: string + IdempotencyKey: + name: Idempotency-Key + in: header + required: false + description: 'Any unique string (a UUID). A retry with the same key and request gets the first answer back.' + schema: + type: string + maxLength: 255 + responses: + ApprovalRequired: + description: 'Held for your approval on your device. Poll `approval.poll_url`, then repeat the same request with `Cbox-Approval`.' + content: + application/json: + schema: + type: object + required: + - error + - message + - approval + properties: + error: + type: string + enum: + - approval_required + message: + type: string + approval: + type: object + required: + - id + - status + - binding_code + - expires_at + - poll_url + properties: + id: + type: string + status: + type: string + binding_code: + type: string + expires_at: + type: string + format: date-time + poll_url: + type: string + format: uri + Unauthorized: + description: 'No valid token of yours. Management keys always get this.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Forbidden: + description: 'The token lacks the required scope.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: 'No such session, passkey, key, device or linked account of yours.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Conflict: + description: 'A request with this `Idempotency-Key` is still being processed (`idempotency_in_progress`). Retry shortly.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UnprocessableEntity: + description: | + Validation failed (`error: validation_failed`, with a field-keyed `errors` map), or the + request was refused — disconnecting your last way to sign in, a key permission you do + not hold. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + TooManyRequests: + description: "The token's rate limit is exhausted." + headers: + Retry-After: + description: 'Seconds to wait before retrying.' + schema: + type: integer + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Profile: + type: object + required: + - id + - name + - email + properties: + id: + type: string + name: + type: + - string + - 'null' + email: + type: + - string + - 'null' + format: email + SessionsRevoked: + type: object + required: + - revoked + properties: + revoked: + type: integer + description: 'How many sessions were signed out.' + PersonalApiKey: + type: object + required: + - id + - organization_id + - client_id + - permissions + properties: + id: + type: string + name: + type: + - string + - 'null' + organization_id: + type: string + client_id: + type: string + prefix: + type: string + permissions: + type: array + items: + type: string + expires_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + token: + type: + - string + - 'null' + description: "The key's value — shown once, on creation; null on an idempotent replay." + Error: + type: object + required: + - error + - message + properties: + error: + type: string + description: 'Stable machine code' + message: + type: string + description: 'Human-readable explanation' + request_id: + type: string + description: 'The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem.' + errors: + type: object + description: 'Present on `validation_failed` only — the offending fields, each mapped to its messages.' + additionalProperties: + type: array + items: + type: string diff --git a/openapi/environment.yaml b/openapi/environment.yaml new file mode 100644 index 0000000..9b6c7bc --- /dev/null +++ b/openapi/environment.yaml @@ -0,0 +1,16936 @@ +# GENERATED by `php artisan openapi:build` from resources/openapi/base/environment.yaml +# and the action registry (app/Actions). Edit the base or the action, never this file. +openapi: 3.1.0 +info: + title: 'Cbox ID — Environment Management API' + version: 1.0.0 + description: | + The **per-environment management plane** — the API an app's own backend uses to run + its tenancy inside ONE environment: organizations and their owners, members, + invitations, roles (in one organization, or everywhere for your staff), apps, APIs, + the API keys customers made for them, and support sessions. It is served on + the environment's own host ({slug}.cboxid.com or a custom domain): the host pins + the environment, and the request is authenticated with an environment API key + (`cbid_env_…`). + + This is distinct from the global organization API (`cbid_ws_…`), which manages the + organization, its environments and members. Credentials never cross planes — an + organization key cannot call this API, and an environment key cannot call the organization + API. An environment key is bound to the host it was minted for: presenting it on + another environment's host fails authentication outright. + + **Scopes.** A key carries an explicit allow-list; every endpoint requires one + (deny-by-default). `resource:read` never implies `resource:write`, so a + read-only integration key cannot mutate. + + **Envelope.** Every success returns `{ "data": … }` (an object, or an array with + a sibling `meta` on lists). Lists are cursor-paginated: pass `?after=` + from the previous page's `meta`. Every error returns + `{ "error": "", "message": "" }`; a validation failure adds a + field-keyed `errors` map alongside the pair. + + **Token Vault** is the exception, and deliberately so: those endpoints authenticate + an OAuth access token, so their 401/403 are RFC 6750 bearer challenges + (`{ "error", "error_description" }` plus `WWW-Authenticate`) rather than this + envelope. Protocol conformance beats local consistency there. + + **Authority.** A management key acts with the ENVIRONMENT's authority — it is the app + vendor's backend, not an organization's administrator. It may therefore grant a staff + role (`tenant_assignable: false`) inside one organization, and create APIs. What an + organization's own administrators may do is unchanged by anything here. Everything a + key does is on the audit trail with the key as the actor (`actor_type: service`). + + **Retries.** Grants (`PUT`), revocations and removals (`DELETE`) are idempotent by + design, and adding a member who already holds the same role answers `200`. To make + organization creation retry-safe, send your own `slug`: a retry then fails with + `422 slug_taken` instead of creating a second organization. + + Endpoints that list the `Idempotency-Key` header take any unique string (a UUID) on a + write: the first successful answer is kept for 24 hours and a retry with the same key + and the same request gets it back unchanged, marked `Idempotent-Replayed: true`. The same + key on a different request is `422 idempotency_key_reused`; a retry that arrives while + the first is still running is `409 idempotency_in_progress`. Refusals are not kept, so a + retry after one runs the request again. + + **Rate limits.** Buckets are keyed on the API KEY, not the caller's IP, so a CI + runner behind a shared NAT gets its own allowance. A `429` carries `Retry-After`. + + **From the platform root.** The people who run a workspace — its team, signed in with + `cbox login` at the platform root (`https://cboxid.com`) — reach every environment of + their workspace from there, with ONE access token (`WorkspaceAccessToken`): call these + same paths on the root's host and name the environment in the `Cbox-Environment` header + (its id or slug). The token runs what the person could run on that environment's + console — their role must administer environments, and their membership must reach + that one — within the scopes it carries. Without the header the answer is + `400 environment_required`; an environment the person cannot reach, or another + workspace's, is `404`. Everything is recorded in that environment's own trail, as the + person (`actor_type: organization_member`), and every critical operation waits for + their approval on their device. +servers: + - + url: 'https://{host}/api/v1' + description: "The environment's own host" + variables: + host: + default: acme.cboxid.com +security: + - + EnvironmentApiKey: [] +tags: + - name: Organizations + - name: Members + - name: Invitations + - name: Roles + - name: Users + - name: Apps + - name: APIs + - name: 'API keys' + - name: 'Support sessions' + - name: Devices + - name: Approvals + - name: Governance + - name: 'Audit log' + - name: 'Audit Logs' + - name: Branding + - name: 'Directory Sync' + - name: Domains + - name: Events + - name: 'Frontend keys' + - name: Hooks + - name: 'Management keys' + - name: 'Legacy login' + - name: 'Log streams' + - name: 'Admin Portal' + - name: Provisioning + - name: 'SAML apps' + - name: Sign-in + - name: 'Enterprise SSO' + - name: 'Token vault' + - name: Webhooks +paths: + /access-reviews: + post: + tags: + - Governance + summary: "Open an access review of one organization's roles and memberships — or of staff roles — for someone to certify or revoke." + description: 'Requires scope `governance:write`. Danger: write.' + operationId: access_reviews_create + x-action: access_reviews.create + x-scope: 'governance:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: 'What the review is: "Q3 finance access".' + maxLength: 190 + covers: + type: string + description: "organization (default): one organization's access, named by organization_id. staff: every environment-wide staff grant." + enum: + - organization + - staff + organization_id: + type: + - string + - 'null' + description: 'The organization to review. Required unless covers is staff.' + maxLength: 64 + due_in_days: + type: integer + description: 'Days until it is due. Default 7.' + minimum: 1 + maximum: 365 + required: + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AccessReview' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Governance + summary: 'List access reviews (certification campaigns), open and closed, optionally those covering one organization.' + description: 'Requires scope `governance:read`. Danger: read.' + operationId: access_reviews_list + x-action: access_reviews.list + x-scope: 'governance:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: 'Only reviews covering this organization: its own and the staff reviews.' + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AccessReview' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/access-reviews/{id}': + get: + tags: + - Governance + summary: 'Read one access review: its status, due date, pending-item policy and item count.' + description: 'Requires scope `governance:read`. Danger: read.' + operationId: access_reviews_get + x-action: access_reviews.get + x-scope: 'governance:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The review's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AccessReview' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/access-reviews/{id}/close': + post: + tags: + - Governance + summary: "Close an access review and apply it: every revoke is carried out, and undecided items get the review's pending policy (revoke by default)." + description: 'Requires scope `governance:write`. Danger: destructive.' + operationId: access_reviews_close + x-action: access_reviews.close + x-scope: 'governance:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The review's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AccessReview' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/access-reviews/{id}/items': + get: + tags: + - Governance + summary: 'List the items an access review asks someone to certify or revoke: who holds what, and the decision on each.' + description: 'Requires scope `governance:read`. Danger: read.' + operationId: access_reviews_items_list + x-action: access_reviews.items.list + x-scope: 'governance:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The review's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AccessReviewItem' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/access-reviews/{id}/items/{item_id}': + post: + tags: + - Governance + summary: 'Certify or revoke one item of an open access review. Revokes are applied when the review closes.' + description: 'Requires scope `governance:write`. Danger: write.' + operationId: access_reviews_items_decide + x-action: access_reviews.items.decide + x-scope: 'governance:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The review's id." + - + name: item_id + in: path + required: true + schema: + type: string + description: "The item's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + decision: + type: string + description: 'certified keeps the access; revoked takes it away when the review closes.' + enum: + - certified + - revoked + note: + type: + - string + - 'null' + description: 'Why, for whoever audits the review.' + maxLength: 1000 + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + required: + - decision + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AccessReviewItem' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/action-approvals/{id}': + get: + tags: + - Approvals + summary: 'Where an approval this key asked for stands' + description: | + Any key may poll the approvals it raised, and only those: another key's id is a 404. + `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + parameters: + - $ref: '#/components/parameters/Id' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + required: + - id + - status + properties: + id: + type: string + status: + type: string + enum: + - pending + - approved + - denied + - expired + - consumed + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + /agent-requests: + get: + tags: + - Approvals + summary: "List the pending requests from agents to act as one of this environment's people (CIBA): which app, for whom, and what it asks." + description: 'Requires scope `approvals:read`. Danger: read.' + operationId: approvals_list + x-action: approvals.list + x-scope: 'approvals:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AgentRequest' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/agent-requests/{request_id}/deny': + post: + tags: + - Approvals + summary: 'Deny a pending approval request (CIBA) for the person it was raised for: the agent gets access_denied and no token.' + description: 'Requires scope `approvals:write`. Danger: destructive.' + operationId: approvals_deny + x-action: approvals.deny + x-scope: 'approvals:write' + x-danger: destructive + parameters: + - + name: request_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/api-keys/{id}': + delete: + operationId: api_keys_revoke + x-action: api_keys.revoke + tags: + - 'API keys' + summary: 'Revoke a member API key' + description: | + Requires scope `api_keys:write`. The key stops verifying at once. Idempotent — an + already-revoked key is a 204 too. Recorded as `api_key.revoked`, with this + management key as the actor. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Revoked. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + /apis: + get: + tags: + - APIs + summary: 'List registered APIs' + description: 'Requires scope `apis:read`.' + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Api' + meta: + $ref: '#/components/schemas/PageMeta' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - APIs + summary: 'Register an API and its scopes' + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + description: | + Requires scope `apis:write`. Registering an API is the environment's act; no tenant + surface can. `identifier` is an absolute URI (RFC 8707) and becomes the token's + `aud`. Scope keys are unique across the environment. `client_id` links the app whose + roles and permissions the API enforces, and must have the API's owner. + `organization_id` makes the API one organization's; left out, the environment owns + it. Refusals are `422 invalid_api` with the reason. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - identifier + - name + properties: + identifier: + type: string + format: uri + maxLength: 255 + name: + type: string + maxLength: 120 + organization_id: + type: + - string + - 'null' + client_id: + type: + - string + - 'null' + scopes: + type: array + items: + $ref: '#/components/schemas/ApiScope' + responses: + 201: + description: Registered. + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Api' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apis/{id}': + get: + tags: + - APIs + summary: 'Get an API' + description: 'Requires scope `apis:read`.' + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Api' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - APIs + summary: 'Change an API' + description: | + Requires scope `apis:write`. Any of: `name`; `client_id` (null unlinks the app); + `scopes`, which is the **complete** set afterwards — scopes named are added or + updated, scopes left out are removed. All or nothing. The identifier never changes: + register a new API instead. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + client_id: + type: + - string + - 'null' + scopes: + type: array + items: + $ref: '#/components/schemas/ApiScope' + responses: + 200: + description: 'The API after the change.' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Api' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + tags: + - APIs + summary: 'Delete an API and its scopes' + description: | + Requires scope `apis:write`. Tokens already minted for it keep their `aud` until they + expire; apps holding its scope keys keep them as plain scopes. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Deleted. + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apis/{id}/scopes/{key}': + put: + tags: + - APIs + summary: 'Add a scope to an API, or change the description or tenant access of one it owns.' + description: 'Requires scope `apis:write`. Danger: write.' + operationId: apis_scopes_define + x-action: apis.scopes.define + x-scope: 'apis:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The API id.' + - + name: key + in: path + required: true + schema: + type: string + description: 'The scope key: `tax:assess`.' + maxLength: 128 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + description: + type: + - string + - 'null' + maxLength: 255 + tenant_requestable: + type: boolean + description: "Whether organizations' own apps may request it. Default true." + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Api' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + tags: + - APIs + summary: 'Remove a scope from an API. Apps holding it keep it as a plain scope that no longer reaches the API.' + description: 'Requires scope `apis:write`. Danger: destructive.' + operationId: apis_scopes_remove + x-action: apis.scopes.remove + x-scope: 'apis:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The API id.' + - + name: key + in: path + required: true + schema: + type: string + description: 'The scope key.' + maxLength: 128 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /approvals: + get: + tags: + - Approvals + summary: 'List pending agent approval requests' + description: | + Requires scope `approvals.read`. The same pending CIBA requests the console's + approvals page shows, for the signed-in subject only. + security: + - + AuthenticatorToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Approval' + meta: + $ref: '#/components/schemas/PageMeta' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 429: + $ref: '#/components/responses/TooManyRequests' + '/approvals/{id}': + get: + tags: + - Approvals + summary: 'Read one pending approval request' + description: | + Requires scope `approvals.read`. This is where `binding_message` is obtained. It + is deliberately absent from the push payload — a lock screen is readable by + anyone holding the phone, and the binding message is the transaction description + CIBA exists to protect — so the app must call this after a notification tap and + render only what it returns. + security: + - + AuthenticatorToken: [] + parameters: + - $ref: '#/components/parameters/Id' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Approval' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/approvals/{id}/approve': + post: + tags: + - Approvals + summary: 'Approve a pending agent request' + description: | + Requires scope `approvals.write`. The approving subject is taken from the access + token, never the body — that binding is the consent step, and the server refuses + a mismatch. + security: + - + AuthenticatorToken: [] + parameters: + - $ref: '#/components/parameters/Id' + responses: + 200: + description: Approved. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/ApprovalDecision' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 409: + $ref: '#/components/responses/ApprovalNotActionable' + 429: + $ref: '#/components/responses/TooManyRequests' + '/approvals/{id}/deny': + post: + tags: + - Approvals + summary: 'Deny a pending agent request' + description: 'Requires scope `approvals.write`.' + security: + - + AuthenticatorToken: [] + parameters: + - $ref: '#/components/parameters/Id' + responses: + 200: + description: Denied. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/ApprovalDecision' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 409: + $ref: '#/components/responses/ApprovalNotActionable' + 429: + $ref: '#/components/responses/TooManyRequests' + /apps: + get: + tags: + - Apps + summary: 'List apps' + description: 'Requires scope `apps:read`. Never a secret.' + operationId: apps_list + x-action: apps.list + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/App' + meta: + $ref: '#/components/schemas/PageMeta' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Apps + summary: 'Register an app' + description: | + Requires scope `apps:write`. Two ways to describe it: + + - **From a blueprint** — send `GET /apps/{id}/blueprint`'s document from another + environment as `blueprint` (promote staging to production). `name`, + `redirect_uris` and `post_logout_redirect_uris` beside it replace the + blueprint's own, since those usually name the environment it came from. A + `private_key_jwt` blueprint needs `jwks`. + - **Short form** — `name` and `type` (`web`, `spa`, `cli`, `service`, `agent`; default + `web`), which decide the client type, grants and default scopes as the console's + "Create app" does. `advanced` takes `client_type` and `grant_types`. + + The response carries `client_secret` for an app that authenticates with one — + **once**. Only its hash is kept, and an idempotent replay returns `client_secret: null`. + + A platform scope reserved for the console (`vault.manage`, `decisions:read`) is + refused (`scope_not_grantable`), as is a registered API's scope this app's owner may + not hold. Danger: critical — it mints a credential. + operationId: apps_create + x-action: apps.create + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + blueprint: + $ref: '#/components/schemas/AppBlueprint' + name: + type: string + maxLength: 190 + type: + type: string + enum: + - web + - spa + - cli + - service + - agent + - advanced + default: web + client_type: + type: string + enum: + - confidential + - public + grant_types: + type: array + items: + type: string + redirect_uris: + type: array + items: + type: string + post_logout_redirect_uris: + type: array + items: + type: string + scopes: + type: array + items: + type: string + first_party: + type: boolean + default: false + manifest_url: + type: + - string + - 'null' + format: uri + maxLength: 500 + description: 'Where the app publishes its roles-and-permissions manifest (short form).' + organization_id: + type: + - string + - 'null' + description: "The owning organization; null for the environment's own app." + jwks: + type: + - object + - 'null' + description: 'A public JWK Set, for a `private_key_jwt` app.' + responses: + 201: + description: Registered. + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}': + delete: + tags: + - Apps + summary: 'Delete an app and every secret it holds. Anything signing in as it stops working immediately.' + description: 'Requires scope `apps:write`. Danger: critical.' + operationId: apps_delete + x-action: apps.delete + x-scope: 'apps:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Apps + summary: 'Get one app (OAuth client) by its id or its client_id: its kind, grants, redirect URIs, scopes and settings. Never a secret.' + description: 'Requires scope `apps:read`. Danger: read.' + operationId: apps_get + x-action: apps.get + x-scope: 'apps:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Apps + summary: 'Rename an app and replace its redirect URIs or sign-out redirect URIs. Fields left out are unchanged.' + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_update + x-action: apps.update + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 190 + redirect_uris: + type: array + description: 'The COMPLETE list afterwards.' + maxItems: 50 + items: + type: string + maxLength: 2048 + post_logout_redirect_uris: + type: array + description: 'The COMPLETE list afterwards.' + maxItems: 50 + items: + type: string + maxLength: 2048 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/blueprint': + get: + tags: + - Apps + summary: "Export an app's configuration as a blueprint" + description: | + Requires scope `apps:read`. `{id}` is the app's id or its `client_id`. The document + carries the app's settings and none of its identity or credentials: no client id, + no secret, no key set, no owning organization. `POST /apps` with it as `blueprint` + creates the same app in another environment. + operationId: apps_blueprint + x-action: apps.blueprint + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AppBlueprint' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/copy': + post: + tags: + - Apps + summary: 'Copy an app into another environment of this project, with a new client id and secret there. A person on the environment console only; a key exports the blueprint instead.' + description: 'Requires scope `apps:write`. Danger: critical.' + operationId: apps_copy + x-action: apps.copy + x-scope: 'apps:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + environment_id: + type: string + description: 'The environment to copy into: another environment of this project.' + maxLength: 64 + name: + type: string + description: 'What to call the copy there.' + minLength: 1 + maxLength: 190 + redirect_uris: + type: array + description: "Its redirect URIs there — staging's callback is rarely production's. Left out, none." + maxItems: 50 + items: + type: string + maxLength: 2048 + required: + - environment_id + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/manifest': + put: + tags: + - Apps + summary: 'Set or clear the URL an app publishes its roles-and-permissions manifest at. Does not fetch it; apps.manifest.sync does.' + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_manifest_set + x-action: apps.manifest.set + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + manifest_url: + type: + - string + - 'null' + description: 'An absolute URL; null or left out clears it.' + format: uri + maxLength: 500 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/manifest/sync': + post: + tags: + - Apps + summary: "Fetch an app's published manifest now and sync the roles and permissions it declares." + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_manifest_sync + x-action: apps.manifest.sync + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/scopes': + put: + tags: + - Apps + summary: "Replace an app's complete scope set — the ceiling of what it may request, applied from its next token." + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_scopes_set + x-action: apps.scopes.set + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + scopes: + type: array + description: 'Required. The COMPLETE scope set afterwards; an empty list removes every scope.' + maxItems: 200 + items: + type: string + maxLength: 128 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/secrets': + get: + tags: + - Apps + summary: "List an app's live client secrets" + description: | + Requires scope `apps:read`. Every live secret, newest first: its id (what + `DELETE /apps/{id}/secrets/{secret_id}` names), the characters it ends in, and its + dates — never a secret. `expires_at` is set on one a rotation replaced, still working + through its overlap. An app holds a handful at most, so there is no page to turn. + operationId: apps_secrets_list + x-action: apps.secrets.list + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AppSecret' + meta: + $ref: '#/components/schemas/PageMeta' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Apps + summary: 'Mint a new client secret for an app and retire the current ones after grace_seconds (0 = at once). The new secret is returned once.' + description: 'Requires scope `apps:write`. Danger: critical.' + operationId: apps_secrets_rotate + x-action: apps.secrets.rotate + x-scope: 'apps:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + grace_seconds: + type: integer + description: 'How long the current secrets keep working, in seconds. 0 stops them at once.' + minimum: 0 + maximum: 2592000 + required: + - grace_seconds + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AppSecret' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/secrets/{secret_id}': + delete: + tags: + - Apps + summary: "Revoke one of an app's client secrets immediately. Never its last live secret — rotate that instead." + description: 'Requires scope `apps:write`. Danger: critical.' + operationId: apps_secrets_revoke + x-action: apps.secrets.revoke + x-scope: 'apps:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - + name: secret_id + in: path + required: true + schema: + type: string + description: "The secret's id, from `GET /apps/{id}/secrets`." + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/settings/api-key-prefix': + put: + tags: + - Apps + summary: "Set the prefix of an app's customer API keys (acme_live), which lets its users create keys for it, or null to stop new keys." + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_settings_api_key_prefix + x-action: apps.settings.api_key_prefix + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + prefix: + type: + - string + - 'null' + description: '2–16 lowercase letters or digits starting with a letter, then `_live` or `_test`. Null or left out: no new keys.' + maxLength: 32 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/settings/backchannel-logout': + put: + tags: + - Apps + summary: 'Set or clear the URI an app is sent a logout token at when somebody signs out (OIDC Back-Channel Logout).' + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_settings_backchannel_logout + x-action: apps.settings.backchannel_logout + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + uri: + type: + - string + - 'null' + description: 'HTTPS (HTTP on localhost only). Null or left out: the app is not told.' + format: uri + maxLength: 2000 + session_required: + type: boolean + description: 'Every logout token carries `sid`. Default false.' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/settings/token-exchange': + put: + tags: + - Apps + summary: 'Turn token exchange (RFC 8693) on or off for a confidential app.' + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_settings_token_exchange + x-action: apps.settings.token_exchange + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + required: + - enabled + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/apps/{id}/settings/token-lifetime': + put: + tags: + - Apps + summary: "Set how long an app's access tokens live, in seconds, or null for the install's default." + description: 'Requires scope `apps:write`. Danger: write.' + operationId: apps_settings_token_lifetime + x-action: apps.settings.token_lifetime + x-scope: 'apps:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The app's id, or its `client_id`." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + access_token_ttl: + type: + - integer + - 'null' + description: "Seconds, from 60 to the install's ceiling. Null or left out: the install's default." + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /audit-log: + get: + tags: + - 'Audit log' + summary: "Read this environment's audit trail oldest first, optionally narrowed by action, actor type or organization; pass the last id as `after` to page." + description: 'Requires scope `audit:read`. Danger: read.' + operationId: audit_list + x-action: audit.list + x-scope: 'audit:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - + name: action + in: query + required: false + schema: + type: string + description: 'Only this action, e.g. `webhook.created`.' + maxLength: 190 + - + name: actor_type + in: query + required: false + schema: + type: string + description: 'Only entries by this kind of actor; `service` is a management key or an app.' + enum: + - user + - service + - system + - operator + - organization_member + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Only this organization's entries." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AuditEntry' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /audit-logs/events: + post: + tags: + - 'Audit Logs' + summary: "Record 1–100 audit events your app's users caused, each for one of your organizations (customers); checked against the action's schema when it has one and appended to that organization's tamper-evident chain. Send an Idempotency-Key." + description: 'Requires scope `audit_logs:write`. Danger: write.' + operationId: audit_logs_events_create + x-action: audit_logs.events.create + x-scope: 'audit_logs:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + events: + type: array + description: '1–100 events, recorded all together or not at all.' + minItems: 1 + maxItems: 100 + items: + type: object + properties: + organization_id: + type: string + description: 'The organization (your customer) the event happened in.' + maxLength: 64 + action: + type: string + description: 'What happened, as dotted words: `invoice.voided`, `user.signed_in`.' + maxLength: 190 + occurred_at: + type: string + description: 'When it happened: ISO 8601 with a time zone, to the millisecond.' + format: date-time + actor: + type: object + properties: + id: + type: string + description: 'Your id for whoever did it.' + maxLength: 190 + type: + type: string + description: 'What kind of actor: `user`, `api_key`, `system`.' + maxLength: 100 + name: + type: + - string + - 'null' + description: 'A name a person reads in the log.' + maxLength: 190 + metadata: + type: object + description: 'Up to 50 names to strings, numbers, booleans or null.' + properties: {} + required: + - id + - type + targets: + type: array + description: 'What it was done to.' + maxItems: 50 + items: + type: object + properties: + id: + type: string + maxLength: 190 + type: + type: string + description: 'What kind of thing: `invoice`, `user`, `report`.' + maxLength: 100 + name: + type: + - string + - 'null' + maxLength: 190 + metadata: + type: object + properties: {} + required: + - id + - type + context: + type: object + properties: + location: + type: + - string + - 'null' + description: "Where it came from — usually the client's IP address." + maxLength: 64 + user_agent: + type: + - string + - 'null' + maxLength: 512 + metadata: + type: object + description: 'Up to 50 names to strings, numbers, booleans or null; values up to 500 characters.' + properties: {} + required: + - organization_id + - action + - occurred_at + - actor + required: + - events + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogEventBatch' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Audit Logs' + summary: 'Read audit events newest first, for one organization or the whole environment, filtered by action, actor, target and time range; pass `next_cursor` as `after` to page.' + description: 'Requires scope `audit_logs:read`. Danger: read.' + operationId: audit_logs_events_list + x-action: audit_logs.events.list + x-scope: 'audit_logs:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Only this organization's events. Required in effect for an organization's own administrator, whose events are the only ones they see." + maxLength: 64 + - + name: actions + in: query + required: false + schema: + type: array + description: 'Only these actions, e.g. `invoice.voided`.' + maxItems: 50 + items: + type: string + maxLength: 190 + - + name: actor_id + in: query + required: false + schema: + type: string + description: 'Only events this actor caused.' + maxLength: 190 + - + name: target_id + in: query + required: false + schema: + type: string + description: 'Only events naming this target.' + maxLength: 190 + - + name: range_start + in: query + required: false + schema: + type: string + description: 'Only events that occurred at or after this time.' + format: date-time + - + name: range_end + in: query + required: false + schema: + type: string + description: 'Only events that occurred before this time.' + format: date-time + - + name: order + in: query + required: false + schema: + type: string + description: '`desc` (default) for newest first, `asc` for oldest first.' + enum: + - desc + - asc + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Events per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + maxLength: 200 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AuditLogEvent' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /audit-logs/exports: + post: + tags: + - 'Audit Logs' + summary: 'Start a CSV export of audit events (same filters as the list). It is written on the queue: poll audit_logs.exports.get until state is ready, then download from its short-lived url.' + description: 'Requires scope `audit_logs:export`. Danger: write.' + operationId: audit_logs_exports_create + x-action: audit_logs.exports.create + x-scope: 'audit_logs:export' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: string + description: "Only this organization's events. Required in effect for an organization's own administrator, whose events are the only ones they see." + maxLength: 64 + actions: + type: array + description: 'Only these actions, e.g. `invoice.voided`.' + maxItems: 50 + items: + type: string + maxLength: 190 + actor_id: + type: string + description: 'Only events this actor caused.' + maxLength: 190 + target_id: + type: string + description: 'Only events naming this target.' + maxLength: 190 + range_start: + type: string + description: 'Only events that occurred at or after this time.' + format: date-time + range_end: + type: string + description: 'Only events that occurred before this time.' + format: date-time + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogExport' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/audit-logs/exports/{id}': + get: + tags: + - 'Audit Logs' + summary: "Read an audit-log export's state; once ready it carries a signed download url valid for a few minutes (read it again for a fresh one)." + description: 'Requires scope `audit_logs:read`. Danger: read.' + operationId: audit_logs_exports_get + x-action: audit_logs.exports.get + x-scope: 'audit_logs:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The export id.' + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogExport' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /audit-logs/schemas: + post: + tags: + - 'Audit Logs' + summary: "Define the schema one audit-log action's events must match — allowed target types and metadata schemas (a subset of JSON Schema). Events of that action are then validated on arrival." + description: 'Requires scope `audit_logs:manage`. Danger: write.' + operationId: audit_logs_schemas_create + x-action: audit_logs.schemas.create + x-scope: 'audit_logs:manage' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + action: + type: string + description: 'The action this schema is for, e.g. `invoice.voided`.' + maxLength: 190 + targets: + type: + - array + - 'null' + description: "The target types this action's events may name, each with an optional metadata schema. Leave out to allow any." + maxItems: 50 + items: + type: object + properties: + type: + type: string + description: 'A target type events of this action may name.' + maxLength: 100 + metadata: + type: object + description: 'A metadata schema for targets of this type.' + properties: {} + required: + - type + actor_metadata: + type: object + description: "A metadata schema (a subset of JSON Schema) for the actor's metadata." + properties: {} + metadata: + type: object + description: "A metadata schema (a subset of JSON Schema) for the event's own metadata." + properties: {} + required: + - action + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogSchema' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Audit Logs' + summary: 'List the audit-log schemas this environment validates events against, one per action.' + description: 'Requires scope `audit_logs:read`. Danger: read.' + operationId: audit_logs_schemas_list + x-action: audit_logs.schemas.list + x-scope: 'audit_logs:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/AuditLogSchema' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/audit-logs/schemas/{action}': + delete: + tags: + - 'Audit Logs' + summary: "Delete an audit-log action's schema. Its events are then accepted unchecked — or refused, if the environment is in strict mode." + description: 'Requires scope `audit_logs:manage`. Danger: destructive.' + operationId: audit_logs_schemas_delete + x-action: audit_logs.schemas.delete + x-scope: 'audit_logs:manage' + x-danger: destructive + parameters: + - + name: action + in: path + required: true + schema: + type: string + description: 'The action whose schema to delete.' + maxLength: 190 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Audit Logs' + summary: "Read the schema one audit-log action's events are validated against." + description: 'Requires scope `audit_logs:read`. Danger: read.' + operationId: audit_logs_schemas_get + x-action: audit_logs.schemas.get + x-scope: 'audit_logs:read' + x-danger: read + parameters: + - + name: action + in: path + required: true + schema: + type: string + description: 'The action, e.g. `invoice.voided`.' + maxLength: 190 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogSchema' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + put: + tags: + - 'Audit Logs' + summary: "Replace an audit-log action's schema with a new version (omitted parts are removed). Recorded events keep the version they were checked against." + description: 'Requires scope `audit_logs:manage`. Danger: write.' + operationId: audit_logs_schemas_update + x-action: audit_logs.schemas.update + x-scope: 'audit_logs:manage' + x-danger: write + parameters: + - + name: action + in: path + required: true + schema: + type: string + description: 'The action whose schema to replace.' + maxLength: 190 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + targets: + type: + - array + - 'null' + description: "The target types this action's events may name, each with an optional metadata schema. Leave out to allow any." + maxItems: 50 + items: + type: object + properties: + type: + type: string + description: 'A target type events of this action may name.' + maxLength: 100 + metadata: + type: object + description: 'A metadata schema for targets of this type.' + properties: {} + required: + - type + actor_metadata: + type: object + description: "A metadata schema (a subset of JSON Schema) for the actor's metadata." + properties: {} + metadata: + type: object + description: "A metadata schema (a subset of JSON Schema) for the event's own metadata." + properties: {} + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogSchema' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /audit-logs/settings: + get: + tags: + - 'Audit Logs' + summary: "Read this environment's audit-log settings: retention in days and whether strict schemas are on." + description: 'Requires scope `audit_logs:read`. Danger: read.' + operationId: audit_logs_settings_get + x-action: audit_logs.settings.get + x-scope: 'audit_logs:read' + x-danger: read + parameters: + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogSettings' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - 'Audit Logs' + summary: "Change this environment's audit-log retention (days, 1–3650) and strict mode. Shortening retention deletes older events at the next daily prune — irreversibly." + description: 'Requires scope `audit_logs:manage`. Danger: destructive.' + operationId: audit_logs_settings_update + x-action: audit_logs.settings.update + x-scope: 'audit_logs:manage' + x-danger: destructive + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + retention_days: + type: integer + description: 'Keep events this many days after they arrive.' + minimum: 1 + maximum: 3650 + strict_schemas: + type: boolean + description: 'Refuse events whose action has no schema.' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogSettings' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /audit-logs/verify: + get: + tags: + - 'Audit Logs' + summary: "Re-hash one organization's audit-event chain (from the oldest event retention kept, or from_sequence) and report whether every event is unchanged and in place." + description: 'Requires scope `audit_logs:read`. Danger: read.' + operationId: audit_logs_verify + x-action: audit_logs.verify + x-scope: 'audit_logs:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: string + description: "The organization whose chain to verify. An organization's own administrator may leave it out." + maxLength: 64 + - + name: from_sequence + in: query + required: false + schema: + type: integer + description: 'Start here rather than at the oldest event kept.' + minimum: 1 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Verify at most this many events. Default 10,000.' + minimum: 1 + maximum: 10000 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/AuditLogVerification' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /branding/appearance: + get: + tags: + - Branding + summary: "Read the hosted sign-in theme: the environment default, or one organization's own." + description: 'Requires scope `branding:read`. Danger: read.' + operationId: branding_appearance_get + x-action: branding.appearance.get + x-scope: 'branding:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Read this organization's theme. Left out, the environment default." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Appearance' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + put: + tags: + - Branding + summary: 'Set the hosted sign-in theme (preset, colours, corners, type, logo) for the environment default or one organization.' + description: 'Requires scope `branding:write`. Danger: write.' + operationId: branding_appearance_set + x-action: branding.appearance.set + x-scope: 'branding:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: 'Theme this organization. Left out, the environment default.' + maxLength: 64 + theme: + type: object + description: 'The theme. Both modes must be readable: text needs 4.5:1 against the background, muted text 3:1.' + properties: + preset: + type: string + description: 'A preset id: cbox, midnight, minimal, warm, contrast.' + maxLength: 64 + radius: + type: string + description: 'Corner radius: 0rem, 0.25rem, 0.375rem, 0.5rem, 0.75rem, 1rem.' + maxLength: 32 + font: + type: string + description: 'Type family: system, geometric, serif.' + maxLength: 32 + light: + type: object + description: "Colours as #rrggbb. One left out takes the preset's." + properties: + primary: + type: string + maxLength: 32 + background: + type: string + maxLength: 32 + foreground: + type: string + maxLength: 32 + muted: + type: string + maxLength: 32 + dark: + type: object + description: "Colours as #rrggbb. One left out takes the preset's." + properties: + primary: + type: string + maxLength: 32 + background: + type: string + maxLength: 32 + foreground: + type: string + maxLength: 32 + muted: + type: string + maxLength: 32 + logo: + type: + - string + - 'null' + description: 'An https URL for the logo; null removes it, left out keeps it.' + format: uri + maxLength: 2048 + required: + - theme + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Appearance' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /branding/whitelabel: + get: + tags: + - Branding + summary: 'Read the white-label branding (palette, app name, email sender, logo) of the environment default or one organization.' + description: 'Requires scope `branding:read`. Danger: read.' + operationId: branding_whitelabel_get + x-action: branding.whitelabel.get + x-scope: 'branding:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Read this organization's branding. Left out, the environment default." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/WhitelabelBranding' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + put: + tags: + - Branding + summary: 'Save the white-label branding (palette, app name, email sender, welcome email) of the environment default or one organization.' + description: 'Requires scope `branding:write`. Danger: write.' + operationId: branding_whitelabel_set + x-action: branding.whitelabel.set + x-scope: 'branding:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: 'Brand this organization. Left out, the environment default.' + maxLength: 64 + palette: + type: object + description: 'Colours as hex (#0a2540) or oklch(…), by token: primary, accent, ring, foreground, background. Sent, it is the complete palette; a token left out or blank is unset.' + properties: + primary: + type: + - string + - 'null' + maxLength: 100 + accent: + type: + - string + - 'null' + maxLength: 100 + ring: + type: + - string + - 'null' + maxLength: 100 + foreground: + type: + - string + - 'null' + maxLength: 100 + background: + type: + - string + - 'null' + maxLength: 100 + app_name: + type: + - string + - 'null' + description: 'The product name shown in place of Cbox ID; null to unset.' + maxLength: 120 + email_from_name: + type: + - string + - 'null' + description: 'The sender name on mail; null to unset.' + maxLength: 120 + email_template: + type: + - string + - 'null' + description: 'The welcome email body; blank or null to use the default.' + maxLength: 5000 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/WhitelabelBranding' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /devices: + post: + tags: + - Devices + summary: 'Enrol a device, or re-enrol after a push-token rotation' + description: | + Requires scope `devices.manage`. Idempotent on `install_id`: posting again with a + fresh `push_token` updates the existing enrolment and returns 200, so the app has + no separate rotation call to get wrong. Re-enrolling also clears circuit-breaker + state and revives a device that was retired by a permanent transport error. + + The device is always bound to the token's own subject. Nothing in the body can + change whose device it is. + security: + - + AuthenticatorToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - install_id + - platform + - push_token + - name + properties: + install_id: + type: string + minLength: 26 + maxLength: 26 + description: | + A ULID the app generates on first launch and keeps in platform secure + storage. Deliberately NOT a vendor device id — `identifierForVendor` + and `ANDROID_ID` both reset on reinstall and are shared across one + vendor's apps. + platform: + type: string + enum: + - ios + - android + push_token: + type: string + maxLength: 4096 + name: + type: string + maxLength: 100 + description: 'Shown in the console. Control characters are stripped.' + app_version: + type: + - string + - 'null' + maxLength: 32 + os_version: + type: + - string + - 'null' + maxLength: 32 + model: + type: + - string + - 'null' + maxLength: 64 + responses: + 200: + description: 'The installation was already enrolled; its details were updated.' + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Device' + 201: + description: Enrolled. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Device' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 409: + description: | + This `install_id` belongs to a different user. Refused rather than reassigned: + silently moving it would let one user capture another's push stream by guessing + an install id. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: install_claimed + message: 'This installation is registered to a different user.' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Devices + summary: "List the caller's own enrolled devices" + description: | + Requires scope `devices.manage`. The push token is never returned — it is a + capability, and nothing outside a delivery attempt needs it. + security: + - + AuthenticatorToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Device' + meta: + $ref: '#/components/schemas/PageMeta' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 429: + $ref: '#/components/responses/TooManyRequests' + '/devices/{id}': + delete: + tags: + - Devices + summary: 'Deregister a device' + description: | + Requires scope `devices.manage`. Another user's device answers exactly as a + missing one does, so this cannot be used to confirm that someone else's handset + exists. + security: + - + AuthenticatorToken: [] + parameters: + - $ref: '#/components/parameters/Id' + responses: + 204: + description: Removed. + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + /directories: + post: + tags: + - 'Directory Sync' + summary: 'Register a SCIM directory for an organization. Answers the SCIM base URL and a bearer token, shown once.' + description: 'Requires scope `directory_sync:write`. Danger: critical.' + operationId: directories_create + x-action: directories.create + x-scope: 'directory_sync:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: string + description: 'The organization whose people it provisions.' + maxLength: 64 + name: + type: string + description: 'What administrators call it: "Okta SCIM".' + maxLength: 120 + required: + - organization_id + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Directory' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Directory Sync' + summary: 'List the directories (SCIM, Google Workspace, Microsoft Entra) syncing people in, optionally for one organization, with their last sync error.' + description: 'Requires scope `directory_sync:read`. Danger: read.' + operationId: directories_list + x-action: directories.list + x-scope: 'directory_sync:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only this organization's." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Directory' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /directories/connect: + post: + tags: + - 'Directory Sync' + summary: "Connect a Google Workspace or Microsoft Entra directory to sync an organization's people from, verifying the credentials first. Runs the first sync." + description: 'Requires scope `directory_sync:write`. Danger: critical.' + operationId: directories_connect + x-action: directories.connect + x-scope: 'directory_sync:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: string + description: 'The organization whose people it provisions.' + maxLength: 64 + provider: + type: string + description: 'google_workspace or microsoft_entra.' + enum: + - google_workspace + - microsoft_entra + credentials: + type: object + description: "The provider's credentials. Write-only: sealed, never returned." + properties: + service_account_json: + type: string + description: "Google: the service account's JSON key, whole." + maxLength: 20000 + admin_email: + type: string + description: 'Google: the administrator the service account acts as.' + maxLength: 190 + tenant_id: + type: string + description: 'Entra: the directory (tenant) id.' + maxLength: 190 + client_id: + type: string + description: "Entra: the app registration's client id." + maxLength: 190 + client_secret: + type: string + description: "Entra: the app registration's client secret." + maxLength: 500 + required: + - organization_id + - provider + - credentials + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Directory' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/directories/{id}': + delete: + tags: + - 'Directory Sync' + summary: 'Delete an inbound directory. Its token stops working; the people it provisioned keep their accounts.' + description: 'Requires scope `directory_sync:write`. Danger: destructive.' + operationId: directories_delete + x-action: directories.delete + x-scope: 'directory_sync:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Directory Sync' + summary: 'Read one inbound directory: its provider, status, SCIM base URL and last sync error. Never its token or credentials.' + description: 'Requires scope `directory_sync:read`. Danger: read.' + operationId: directories_get + x-action: directories.get + x-scope: 'directory_sync:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Directory' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - 'Directory Sync' + summary: 'Rename an inbound directory.' + description: 'Requires scope `directory_sync:write`. Danger: write.' + operationId: directories_update + x-action: directories.update + x-scope: 'directory_sync:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + name: + type: string + description: 'The new name.' + maxLength: 120 + required: + - name + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Directory' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/directories/{id}/group-roles': + post: + tags: + - 'Directory Sync' + summary: 'Map a directory group onto a role (everyone in the group holds it as membership syncs), or unmap it.' + description: 'Requires scope `directory_sync:write`. Danger: write.' + operationId: directories_groups_map + x-action: directories.groups.map + x-scope: 'directory_sync:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + group_id: + type: string + description: "One of the directory's groups." + maxLength: 64 + role_id: + type: string + description: "A role the organization's own people may hold." + maxLength: 64 + mapped: + type: boolean + description: 'true to map the group onto the role, false to unmap it.' + required: + - group_id + - role_id + - mapped + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/DirectoryGroup' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/directories/{id}/groups': + get: + tags: + - 'Directory Sync' + summary: 'List the groups a directory has synced, with the role ids each group is mapped onto.' + description: 'Requires scope `directory_sync:read`. Danger: read.' + operationId: directories_groups_list + x-action: directories.groups.list + x-scope: 'directory_sync:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/DirectoryGroup' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/directories/{id}/rotate': + post: + tags: + - 'Directory Sync' + summary: 'Issue a new bearer token for a SCIM directory, returned once. The old token stops working immediately.' + description: 'Requires scope `directory_sync:write`. Danger: critical.' + operationId: directories_token_rotate + x-action: directories.token.rotate + x-scope: 'directory_sync:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Directory' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/directories/{id}/status': + post: + tags: + - 'Directory Sync' + summary: "Pause or resume an inbound directory's provisioning." + description: 'Requires scope `directory_sync:write`. Danger: write.' + operationId: directories_status_set + x-action: directories.status.set + x-scope: 'directory_sync:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The directory's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: 'true to resume provisioning, false to pause it.' + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + required: + - active + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Directory' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /domains: + post: + tags: + - Domains + summary: 'Start serving this environment on a custom domain: returns the DNS TXT record that proves you control it.' + description: 'Requires scope `domains:write`. Danger: write.' + operationId: domains_add + x-action: domains.add + x-scope: 'domains:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + domain: + type: string + description: 'The domain, for example login.example.com.' + maxLength: 253 + required: + - domain + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/CustomDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Domains + summary: "Read this environment's custom domain, and the DNS TXT record that proves a pending one." + description: 'Requires scope `domains:read`. Danger: read.' + operationId: domains_get + x-action: domains.get + x-scope: 'domains:read' + x-danger: read + parameters: + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/CustomDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + tags: + - Domains + summary: 'Stop serving this environment on its custom domain. Anything using that domain stops reaching the environment.' + description: 'Requires scope `domains:write`. Danger: destructive.' + operationId: domains_remove + x-action: domains.remove + x-scope: 'domains:write' + x-danger: destructive + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /domains/verify: + post: + tags: + - Domains + summary: "Look for the pending domain's DNS TXT record and, once it is visible, serve this environment on that domain." + description: 'Requires scope `domains:write`. Danger: write.' + operationId: domains_verify + x-action: domains.verify + x-scope: 'domains:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/CustomDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /events: + get: + tags: + - Events + summary: "Read this environment's domain events (the facts webhooks deliver) oldest first; pass the last id as `after` to poll for new ones." + description: 'Requires scope `events:read`. Danger: read.' + operationId: events_list + x-action: events.list + x-scope: 'events:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - + name: types + in: query + required: false + schema: + type: array + description: 'Only these event types, e.g. `user.created`.' + maxItems: 50 + items: + type: string + maxLength: 190 + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Only this organization's events." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/DomainEvent' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /frontend-keys: + post: + tags: + - 'Frontend keys' + summary: 'Create a publishable frontend key for a browser app, usable only from the origins listed. The key is public by design.' + description: 'Requires scope `frontend_keys:write`. Danger: write.' + operationId: frontend_keys_create + x-action: frontend_keys.create + x-scope: 'frontend_keys:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: 'What the key is for, so a person recognises it later.' + maxLength: 120 + mode: + type: string + description: "`test` or `live`; it is in the key's prefix." + enum: + - test + - live + origins: + type: array + description: 'The exact origins (scheme, host, port) allowed to present the key, for example https://app.example.com. At least one.' + minItems: 1 + maxItems: 50 + items: + type: string + maxLength: 255 + required: + - name + - mode + - origins + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/FrontendKey' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Frontend keys' + summary: 'List the publishable frontend keys browser apps present to the Frontend API, in full, with their allowed origins.' + description: 'Requires scope `frontend_keys:read`. Danger: read.' + operationId: frontend_keys_list + x-action: frontend_keys.list + x-scope: 'frontend_keys:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/FrontendKey' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/frontend-keys/{id}': + delete: + tags: + - 'Frontend keys' + summary: 'Revoke a publishable frontend key. Pages still holding it stop working immediately.' + description: 'Requires scope `frontend_keys:write`. Danger: destructive.' + operationId: frontend_keys_revoke + x-action: frontend_keys.revoke + x-scope: 'frontend_keys:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The key's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/frontend-keys/{id}/origins': + put: + tags: + - 'Frontend keys' + summary: 'Replace the exact list of origins allowed to present a publishable frontend key. Live on the next request.' + description: 'Requires scope `frontend_keys:write`. Danger: write.' + operationId: frontend_keys_set_origins + x-action: frontend_keys.set_origins + x-scope: 'frontend_keys:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The key's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + origins: + type: array + description: 'The exact origins (scheme, host, port) allowed to present the key, for example https://app.example.com. At least one.' + minItems: 1 + maxItems: 50 + items: + type: string + maxLength: 255 + required: + - origins + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/FrontendKey' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /hooks: + post: + tags: + - Hooks + summary: 'Register a hook at a hook point (token minting, login, registration, password change). Returns its signing secret once.' + description: 'Requires scope `hooks:write`. Danger: critical.' + operationId: hooks_create + x-action: hooks.create + x-scope: 'hooks:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + hook_point: + type: string + description: 'When it is called.' + enum: + - token_minting + - post_login + - pre_registration + - post_registration + - pre_password_change + - post_password_change + url: + type: string + description: 'A public HTTPS URL, called in the middle of the operation.' + format: uri + maxLength: 500 + organization_id: + type: + - string + - 'null' + description: "The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide." + maxLength: 64 + environment_wide: + type: boolean + description: "True to make it the environment's own, carrying EVERY organization's traffic. Send this or organization_id." + required: + - hook_point + - url + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/InlineHook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Hooks + summary: 'List the hooks — endpoints called during sign-in and token issuance — with their hook point, owner and whether each is active.' + description: 'Requires scope `hooks:read`. Danger: read.' + operationId: hooks_list + x-action: hooks.list + x-scope: 'hooks:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/InlineHook' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/hooks/{id}': + delete: + tags: + - Hooks + summary: 'Remove a hook. It is no longer called at its hook point.' + description: 'Requires scope `hooks:write`. Danger: destructive.' + operationId: hooks_delete + x-action: hooks.delete + x-scope: 'hooks:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The hook id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Hooks + summary: 'Get one hook: its URL, hook point, owner and whether it is active. Never its signing secret.' + description: 'Requires scope `hooks:read`. Danger: read.' + operationId: hooks_get + x-action: hooks.get + x-scope: 'hooks:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The hook id.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/InlineHook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Hooks + summary: 'Pause (active: false) or activate (active: true) a hook.' + description: 'Requires scope `hooks:write`. Danger: critical.' + operationId: hooks_update + x-action: hooks.update + x-scope: 'hooks:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The hook id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: 'True to call it at its hook point; false to pause it.' + required: + - active + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/InlineHook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /keys: + post: + tags: + - 'Management keys' + summary: 'Mint a management key for this environment, at most as wide as the caller. The value is returned once, as `token`.' + description: 'Requires scope `keys:write`. Danger: critical.' + operationId: keys_create + x-action: keys.create + x-scope: 'keys:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + scopes: + type: array + description: 'The scopes the key carries. From a key: a subset of its own.' + minItems: 1 + maxItems: 64 + items: + type: string + maxLength: 64 + expires_at: + type: + - string + - 'null' + description: 'When it stops working. From a key: required to be no later than the parent; defaults to the latest allowed.' + format: date-time + description: + type: + - string + - 'null' + description: 'What it is for — shown beside it in the console.' + maxLength: 500 + require_approval: + type: object + description: "Which of this key's actions need its owner's approval on their device first. From a key: never weaker than its own." + properties: + min_danger: + type: + - string + - 'null' + description: "Every action at or above this danger waits for a person's approval." + enum: + - write + - destructive + - critical + actions: + type: array + description: 'These actions wait for approval whatever their danger.' + maxItems: 100 + items: + type: string + maxLength: 100 + required: + - name + - scopes + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Management keys' + summary: "List this environment's management keys (names, scopes, parents, expiry, last use — never their values)." + description: 'Requires scope `keys:read`. Danger: read.' + operationId: keys_list + x-action: keys.list + x-scope: 'keys:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + type: object + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/keys/{id}': + delete: + tags: + - 'Management keys' + summary: 'Revoke a management key and every key it minted, immediately.' + description: 'Requires scope `keys:write`. Danger: destructive.' + operationId: keys_revoke + x-action: keys.revoke + x-scope: 'keys:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The key to revoke.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/keys/{id}/rotate': + post: + tags: + - 'Management keys' + summary: 'Rotate a management key: mint a successor with the same scopes and retire the old one after a grace period.' + description: 'Requires scope `keys:write`. Danger: critical.' + operationId: keys_rotate + x-action: keys.rotate + x-scope: 'keys:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The key to rotate.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + grace_hours: + type: integer + description: 'How long the old key keeps working. Default 24.' + minimum: 0 + maximum: 168 + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /legacy-login: + get: + tags: + - 'Legacy login' + summary: 'Read the legacy login endpoint an app declared for migration, and whether it is approved to receive sign-ins.' + description: 'Requires scope `signin:read`. Danger: read.' + operationId: legacy_login_get + x-action: legacy_login.get + x-scope: 'signin:read' + x-danger: read + parameters: + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LegacyLogin' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /legacy-login/approve: + post: + tags: + - 'Legacy login' + summary: 'Approve the declared legacy login endpoint: sign-ins for people not yet migrated, with their passwords, go to that URL.' + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: legacy_login_approve + x-action: legacy_login.approve + x-scope: 'signin:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LegacyLogin' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /legacy-login/probe: + post: + tags: + - 'Legacy login' + summary: 'Ask the declared legacy login endpoint whether it knows an address — your own — to test it before approving. Sends no password.' + description: 'Requires scope `signin:write`. Danger: write.' + operationId: legacy_login_probe + x-action: legacy_login.probe + x-scope: 'signin:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + description: 'An address that exists in your old system — your own.' + format: email + maxLength: 254 + required: + - email + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /legacy-login/revoke: + post: + tags: + - 'Legacy login' + summary: 'Withdraw the legacy login approval. People not yet migrated can no longer sign in; the declaration stays on file.' + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: legacy_login_revoke + x-action: legacy_login.revoke + x-scope: 'signin:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LegacyLogin' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /log-streams: + post: + tags: + - 'Log streams' + summary: 'Stream the audit trail to a SIEM (Splunk, Elastic, Graylog, CEF, JSON). A generated HMAC key is returned once.' + description: 'Requires scope `log_streams:write`. Danger: critical.' + operationId: log_streams_create + x-action: log_streams.create + x-scope: 'log_streams:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 190 + destination: + type: string + enum: + - splunk_hec + - elastic_ecs + - graylog_gelf + - cef_http + - generic_json + endpoint_url: + type: string + description: 'A public URL the entries are POSTed to.' + format: uri + maxLength: 2048 + auth: + type: string + description: "How the endpoint is authenticated. Left out, the destination's default." + enum: + - none + - bearer + - splunk + - hmac + secret: + type: + - string + - 'null' + description: 'The bearer or Splunk token the endpoint expects. Left out with `hmac`, a key is generated and returned once.' + maxLength: 4096 + organization_id: + type: + - string + - 'null' + description: "The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide." + maxLength: 64 + environment_wide: + type: boolean + description: "True to make it the environment's own, carrying EVERY organization's traffic. Send this or organization_id." + required: + - name + - destination + - endpoint_url + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LogStream' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Log streams' + summary: "List the SIEM destinations this environment's audit trail is streamed to, and whether each is enabled. Never a secret." + description: 'Requires scope `log_streams:read`. Danger: read.' + operationId: log_streams_list + x-action: log_streams.list + x-scope: 'log_streams:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/LogStream' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/log-streams/{id}': + delete: + tags: + - 'Log streams' + summary: 'Delete an audit log stream. Nothing more is delivered to its endpoint.' + description: 'Requires scope `log_streams:write`. Danger: destructive.' + operationId: log_streams_delete + x-action: log_streams.delete + x-scope: 'log_streams:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The log stream id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Log streams' + summary: 'Get one audit log stream: destination, endpoint, auth scheme, owner and health. Never its secret.' + description: 'Requires scope `log_streams:read`. Danger: read.' + operationId: log_streams_get + x-action: log_streams.get + x-scope: 'log_streams:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The log stream id.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LogStream' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - 'Log streams' + summary: 'Disable (enabled: false) or resume (enabled: true) an audit log stream. Disabled, entries are kept and delivered on resume.' + description: 'Requires scope `log_streams:write`. Danger: critical.' + operationId: log_streams_update + x-action: log_streams.update + x-scope: 'log_streams:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The log stream id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + description: 'True to deliver; false to stop, keeping what is pending.' + required: + - enabled + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LogStream' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/log-streams/{id}/test': + post: + tags: + - 'Log streams' + summary: 'Send one test entry to a log stream now and report whether the SIEM accepted it.' + description: 'Requires scope `log_streams:write`. Danger: write.' + operationId: log_streams_test + x-action: log_streams.test + x-scope: 'log_streams:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The log stream's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/LogStreamTest' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /oauth/api-keys/verify: + servers: + - + url: 'https://{host}' + description: "The environment's own host" + variables: + host: + default: acme.cboxid.com + post: + tags: + - 'API keys' + summary: 'Verify an API key presented to your API' + description: | + Your API received an API key (`{prefix}_…`) and asks whether it is good and + what it may do. Authenticate as **your app**, exactly as at the token endpoint — + `client_secret_basic`, `client_secret_post` or `private_key_jwt`; a public client is + refused. Send the key as `key`. + + A live key bound to your app answers with its holder, organization, tier and + permissions (`permissions` is re-capped at every call to what the holder still has). + **Everything else** — unknown, malformed, revoked, expired, another app's key, a + holder who left — is exactly `{"active": false}`: the endpoint never says which. + `401` is only ever about your own client credentials. The response is not cacheable. + security: + - + AppClientCredentials: [] + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ApiKeyVerificationRequest' + application/json: + schema: + $ref: '#/components/schemas/ApiKeyVerificationRequest' + responses: + 200: + description: 'The verdict.' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyVerification' + 401: + description: "Your app's client credentials were missing or wrong (RFC 6749 §5.2)." + headers: + WWW-Authenticate: + description: 'Basic realm="api-keys"' + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/OAuthClientError' + 429: + description: 'Too many verifications from this address — `cbox-id.customer_api_keys.verify_per_minute`, default 600 a minute.' + headers: + Retry-After: + description: 'Seconds to wait before retrying.' + schema: + type: integer + /organizations: + get: + operationId: organizations_list + x-action: organizations.list + tags: + - Organizations + summary: 'List organizations' + description: | + Requires scope `organizations:read`. A page at a time, archived ones included. Narrow + with `q` (a fragment of the name or slug) or `status`. + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - + name: q + in: query + required: false + schema: + type: string + maxLength: 190 + - + name: status + in: query + required: false + schema: + type: string + enum: + - active + - suspended + - deleted + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 400: + $ref: '#/components/responses/EnvironmentRequired' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Organization' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + operationId: organizations_create + x-action: organizations.create + tags: + - Organizations + summary: 'Create an organization, optionally with its owner' + description: 'Requires scope `organizations:write`.' + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + maxLength: 190 + slug: + type: + - string + - 'null' + maxLength: 190 + description: | + URL-safe; unique within the environment. Optional — left out, it is derived + from the name and made unique (`acme`, `acme-2`, …). **Send one if you + retry creates**: with your own slug a retried request is a `422 + slug_taken` you can recognise, not a second organization. + parent_id: + type: + - string + - 'null' + description: 'An organization in this environment to place it under (`422 parent_not_found` otherwise).' + owner_user_id: + type: + - string + - 'null' + description: | + An existing user of this environment who becomes the organization's + **owner**, in the same transaction as the create (`422 user_not_found` + otherwise). Without it the organization starts with no owner; make one + later with `transfer-ownership`. + type: + type: string + enum: + - customer + - reseller + default: customer + description: '`reseller` manages customer organizations beneath it in the tree; `customer` is a leaf tenant.' + metadata: + type: + - object + - 'null' + additionalProperties: + type: string + maxLength: 500 + description: 'Free-form text values you keep on the organization.' + responses: + 201: + description: Created + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{id}': + get: + operationId: organizations_get + x-action: organizations.get + tags: + - Organizations + summary: 'Get an organization' + description: 'Requires scope `organizations:read`.' + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + operationId: organizations_update + x-action: organizations.update + tags: + - Organizations + summary: 'Rename an organization, change its slug, or replace its metadata' + description: | + Requires scope `organizations:write`. A field left out is left alone; a request + that changes nothing answers with the organization as it is and records nothing. + A change is announced as the `organization.updated` webhook and recorded on the + organization's trail as `organization.renamed`, with the values before and after. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 190 + slug: + type: string + minLength: 1 + maxLength: 190 + description: 'URL-safe; unique within the environment.' + metadata: + type: + - object + - 'null' + additionalProperties: + type: string + maxLength: 500 + description: 'Replaces the metadata as a whole; `null` or `{}` clears it.' + responses: + 200: + description: 'The organization after the change.' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + operationId: organizations_delete + x-action: organizations.delete + tags: + - Organizations + summary: 'Archive an organization' + description: | + Requires scope `organizations:write`. **Archives** rather than erases: the status + becomes `deleted`, every member loses access from their next request, the rows stay + for the audit trail, and `organization.deleted` is announced. Idempotent — archiving + an archived organization answers with it unchanged. + + Refused with `409 owns_products` for an organization that owns identity-provider + projects on this platform (a platform customer, not a tenant of your app). + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: 'The archived organization.' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{id}/reactivate': + post: + tags: + - Organizations + summary: "Lift an organization's suspension so its members can sign in again." + description: 'Requires scope `organizations:write`. Danger: write.' + operationId: organizations_reactivate + x-action: organizations.reactivate + x-scope: 'organizations:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{id}/suspend': + post: + tags: + - Organizations + summary: 'Suspend an organization: its members cannot sign in to it until it is reactivated.' + description: 'Requires scope `organizations:write`. Danger: write.' + operationId: organizations_suspend + x-action: organizations.suspend + x-scope: 'organizations:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{id}/transfer-ownership': + post: + operationId: organizations_transfer_ownership + x-action: organizations.transfer_ownership + tags: + - Organizations + summary: "Make a member the organization's owner" + description: | + Requires scope `organizations:write`. The new owner must be an **active** member. + With one current owner this is a hand-over: the previous owner stays on as `admin`. + An organization with no owner (created without `owner_user_id`) gets its first; one + with several (from before ownership was transfer-only) ends with exactly one, the + others stepping down to `admin`. + + Refusals: `422 not_a_member`, `409 not_active`, `409 already_owner`. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - user_id + properties: + user_id: + type: string + responses: + 200: + description: "The new owner's membership." + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/api-keys': + get: + operationId: api_keys_list + x-action: api_keys.list + tags: + - 'API keys' + summary: "List the API keys an organization's members created for your apps" + description: | + Requires scope `api_keys:read`. Revoked and expired keys are included, marked by + `status`. Never the key itself. Narrow to one app with `?client_id=`. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - + name: client_id + in: query + required: false + schema: + type: string + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/ApiKey' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/domains': + get: + operationId: organizations_domains_list + x-action: organizations.domains.list + tags: + - Organizations + summary: 'List the email domains an organization claims' + description: | + Requires scope `organizations:read`. Verified or not, each with the DNS TXT record + that proves it (`record_name`, `record_value`). A handful at most; not paged. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/OrganizationDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Organizations + summary: 'Claim an email domain (acme.com) for an organization. Returns the DNS TXT record to publish before verifying it.' + description: 'Requires scope `organizations:write`. Danger: write.' + operationId: organizations_domains_add + x-action: organizations.domains.add + x-scope: 'organizations:write' + x-danger: write + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + domain: + type: string + description: 'A domain name — acme.com, not an address or a URL.' + maxLength: 190 + required: + - domain + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/OrganizationDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/domains/{domain_id}': + delete: + tags: + - Organizations + summary: 'Remove a claimed email domain from an organization. Capture on it ends.' + description: 'Requires scope `organizations:write`. Danger: destructive.' + operationId: organizations_domains_remove + x-action: organizations.domains.remove + x-scope: 'organizations:write' + x-danger: destructive + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - + name: domain_id + in: path + required: true + schema: + type: string + description: "The claimed domain's id." + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/domains/{domain_id}/capture': + put: + tags: + - Organizations + summary: "Turn capture on or off for a verified domain: with it on, everyone with an address there signs in through the organization's SSO." + description: 'Requires scope `organizations:write`. Danger: critical.' + operationId: organizations_domains_capture + x-action: organizations.domains.capture + x-scope: 'organizations:write' + x-danger: critical + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - + name: domain_id + in: path + required: true + schema: + type: string + description: "The claimed domain's id." + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + description: 'Whether the domain captures sign-ins.' + required: + - enabled + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/OrganizationDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/domains/{domain_id}/verify': + post: + tags: + - Organizations + summary: "Check a claimed domain's DNS TXT record and mark the domain verified if it is published." + description: 'Requires scope `organizations:write`. Danger: write.' + operationId: organizations_domains_verify + x-action: organizations.domains.verify + x-scope: 'organizations:write' + x-danger: write + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - + name: domain_id + in: path + required: true + schema: + type: string + description: "The claimed domain's id." + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/OrganizationDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/invitations': + get: + operationId: invitations_list + x-action: invitations.list + tags: + - Invitations + summary: "List an organization's pending invitations" + description: 'Requires scope `invitations:read`. Pending and unexpired only.' + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Invitation' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + operationId: invitations_send + x-action: invitations.send + tags: + - Invitations + summary: 'Invite someone to the organization' + description: | + Requires scope `invitations:write`. Mails a link; the person joins when they open it + and confirm. Inviting an address again replaces its earlier pending invitation. + + - `role` — `admin` or `member` (default `member`). Never `owner`. + - `roles` — access roles granted when the invitation is accepted: role ids, or your + app's manifest **keys** when `client_id` names the app that declared them. Only + roles the organization's own administrators could grant are accepted: a **staff + role** is refused (`422 role_not_assignable`) — grant it after they join, with + `PUT …/members/{user_id}/roles/{role_id}`. + - `client_id` + `return_to` — after accepting, the person is sent to `return_to`, + which must be on one of that app's registered redirect-URI origins (checked again + when the invitation is accepted). + - `inviter_name` — who the mail says it is from. Defaults to the app's name, or the + environment's. + + Refusals: `409 already_member`, `409 access_role_conflict` (segregation of duties), + `422 unknown_role`, `422 role_not_assignable`, `422 unknown_app`, + `422 return_without_app`, `422 return_to_malformed`, `422 return_to_not_registered`, + `503 mail_failed` (nothing was created — retry). + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + format: email + maxLength: 190 + role: + $ref: '#/components/schemas/AssignableMemberRole' + roles: + type: array + maxItems: 50 + items: + type: string + client_id: + type: + - string + - 'null' + return_to: + type: + - string + - 'null' + format: uri + maxLength: 2048 + inviter_name: + type: + - string + - 'null' + maxLength: 120 + responses: + 201: + description: Sent. + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Invitation' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + 503: + $ref: '#/components/responses/MailFailed' + '/organizations/{organization_id}/invitations/{invitation_id}': + delete: + operationId: invitations_revoke + x-action: invitations.revoke + tags: + - Invitations + summary: 'Withdraw a pending invitation' + description: | + Requires scope `invitations:write`. The link stops working, and the roles parked + for it go with it. `409 not_pending` for one already accepted, withdrawn or expired. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/InvitationId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Withdrawn. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/invitations/{invitation_id}/resend': + post: + operationId: invitations_resend + x-action: invitations.resend + tags: + - Invitations + summary: 'Mail a pending invitation again' + description: | + Requires scope `invitations:write`. On a **fresh link**: only a hash of the old one is + kept, so it cannot be re-sent, and the reason people ask is usually that it expired. + The response is the NEW invitation — its `id` replaces the old one, which stops + working. Its roles and app context move with it. + + One re-send per address per minute (`429 too_soon`); `409 not_pending` for one that + was accepted, withdrawn or has expired; `503 mail_failed` keeps the invitation. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/InvitationId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: 'Sent again.' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Invitation' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 429: + $ref: '#/components/responses/TooManyRequests' + 503: + $ref: '#/components/responses/MailFailed' + '/organizations/{organization_id}/members': + get: + operationId: members_list + x-action: members.list + tags: + - Members + summary: "List an organization's members" + description: 'Requires scope `members:read`. Every membership, whatever its status, oldest first.' + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Member' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + operationId: members_add + x-action: members.add + tags: + - Members + summary: 'Add an existing user as a member' + description: | + Requires scope `members:write`. Straight in — no invitation, no mail. Name the user by + `user_id` or by `email`; they must exist in this environment (`422 user_not_found`). + A suspended or archived organization takes no new members (`409 organization_inactive`). + + `roles` grants access roles at the same time — ids, or your app's manifest keys with + `client_id` — with the environment's authority (staff roles included). One the + organization cannot hold is `422 role_not_assignable`; a segregation-of-duties + conflict is `409 role_conflict`, and nothing is added. + + `role` is `admin` or `member` (default `member`). `owner` is never assignable: + ownership moves with `transfer-ownership`. + + **Idempotent for the same role**: repeating the request answers `200` with the + existing membership. A member on a *different* role is `409 already_member` — change + the role with `PATCH`. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + email: + type: string + format: email + description: 'Instead of `user_id`.' + role: + $ref: '#/components/schemas/AssignableMemberRole' + roles: + type: array + maxItems: 50 + items: + type: string + client_id: + type: + - string + - 'null' + description: 'The app whose manifest keys `roles` are.' + responses: + 200: + description: 'Already a member with that role — nothing changed.' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Member' + 201: + description: Added. + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/members/{user_id}': + patch: + operationId: members_update + x-action: members.update + tags: + - Members + summary: "Change a member's role" + description: | + Requires scope `members:write`. `role` is `admin` or `member`. Demoting the only + owner is refused with `409 last_owner` — transfer ownership first. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/UserId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - role + properties: + role: + $ref: '#/components/schemas/AssignableMemberRole' + responses: + 200: + description: 'The membership after the change.' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + operationId: members_remove + x-action: members.remove + tags: + - Members + summary: 'Remove a member' + description: | + Requires scope `members:write`. The member's roles in the organization go with the + membership. Removing the only owner is refused with `409 last_owner`. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/UserId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Removed. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/members/{user_id}/roles': + get: + operationId: members_roles_list + x-action: members.roles.list + tags: + - Roles + summary: 'List the roles a member holds in the organization' + description: | + Requires scope `roles:read`. Roles granted AT this organization. Roles a person holds + everywhere in the environment (staff) are listed under + `/users/{id}/environment-roles`. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/UserId' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/RoleAssignment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/members/{user_id}/roles/{role_id}': + put: + operationId: members_roles_grant + x-action: members.roles.grant + tags: + - Roles + summary: 'Grant a member a role in the organization' + description: | + Requires scope `roles:write`. Idempotent. + + **This API grants with the environment's authority.** It is your backend, so it may + grant any role the organization can hold — including a **staff role** + (`tenant_assignable: false`), such as your support lead's "Support" role at one + customer. An organization's own administrators still cannot: their console, their + invitations and their directory mappings refuse staff roles, and nothing here + changes that. + + Refusals: `422 role_not_assignable` (another organization's role, or one from an app + this organization cannot use), `409 role_conflict` (segregation of duties). + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/UserId' + - $ref: '#/components/parameters/RoleId' + - $ref: '#/components/parameters/RoleClientId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: 'Granted (or already held).' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/RoleAssignment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + operationId: members_roles_revoke + x-action: members.roles.revoke + tags: + - Roles + summary: 'Take a role back from a member' + description: 'Requires scope `roles:write`. Idempotent — a role the member does not hold is a 204 too.' + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/UserId' + - $ref: '#/components/parameters/RoleId' + - $ref: '#/components/parameters/RoleClientId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: 'Not held any more.' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/organizations/{organization_id}/portal-links': + post: + tags: + - 'Admin Portal' + summary: "Create a one-time Admin Portal link an organization's IT administrator uses to set up SSO, directory sync, domain verification, log streams or SAML certificate renewal, or to read its audit logs, without an account. The URL is shown once." + description: 'Requires scope `portal_links:write`. Danger: critical.' + operationId: organizations_portal_links_create + x-action: organizations.portal_links.create + x-scope: 'portal_links:write' + x-danger: critical + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization the link sets up.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + intents: + type: array + description: "What the link may set up: sso (connection and email domains), dsync (directory sync over SCIM), domain_verification, log_streams, certificate_renewal (a SAML connection's signing certificate), audit_logs (the organization's audit events, read-only, with CSV export)." + minItems: 1 + items: + type: string + enum: + - sso + - dsync + - domain_verification + - log_streams + - certificate_renewal + - audit_logs + expires_in_minutes: + type: + - integer + - 'null' + description: "How long the link may wait to be opened, in minutes — 5 to 10080 (a week). Left out, the deployment's default (30)." + minimum: 5 + maximum: 10080 + email: + type: + - string + - 'null' + description: "Mail the link to this address — the customer's IT contact. Left out, nothing is sent." + format: email + maxLength: 254 + locale: + type: + - string + - 'null' + description: "The language of that mail. Left out, the environment's default language." + enum: + - en + - da + - de + - sv + - nb + - fr + required: + - intents + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PortalLink' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /permissions: + post: + tags: + - Roles + summary: "Author a manual `feature:action` permission, shared with the environment (organization_id null) or one organization's own." + description: 'Requires scope `role_definitions:write`. Danger: write.' + operationId: permissions_create + x-action: permissions.create + x-scope: 'role_definitions:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: 'The key: a feature and an action joined by a colon, like invoices:create.' + maxLength: 120 + description: + type: + - string + - 'null' + maxLength: 500 + organization_id: + type: + - string + - 'null' + description: "The organization it belongs to; null for the environment's shared tier." + maxLength: 64 + tenant_assignable: + type: boolean + description: "Shared tier only: whether organizations may compose it into their own roles. Default false; always true on an organization's own." + required: + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Permission' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Roles + summary: "List the permission catalogue roles are composed from, a page at a time. Narrow by `client_id` (one app's), `organization_id` (what it can see) or a `q` fragment." + description: 'Requires scope `roles:read`. Danger: read.' + operationId: permissions_list + x-action: permissions.list + x-scope: 'roles:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - + name: organization_id + in: query + required: false + schema: + type: string + description: 'Also the permissions this organization authored for itself.' + maxLength: 64 + - + name: client_id + in: query + required: false + schema: + type: string + description: 'Only the permissions this app declared.' + maxLength: 255 + - + name: q + in: query + required: false + schema: + type: string + description: 'Only permissions whose key or description contains this.' + maxLength: 120 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Permission' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/permissions/{id}': + delete: + tags: + - Roles + summary: 'Delete a manual permission: every role carrying it loses it first.' + description: 'Requires scope `role_definitions:write`. Danger: destructive.' + operationId: permissions_delete + x-action: permissions.delete + x-scope: 'role_definitions:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The permission id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Roles + summary: "Change a manual permission's description, and on the shared tier whether organizations may use it. The key itself never changes." + description: 'Requires scope `role_definitions:write`. Danger: write.' + operationId: permissions_update + x-action: permissions.update + x-scope: 'role_definitions:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The permission id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + description: + type: + - string + - 'null' + maxLength: 500 + tenant_assignable: + type: boolean + description: 'Shared tier only: whether organizations may compose it into their own roles.' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Permission' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /provisioning-targets: + post: + tags: + - Provisioning + summary: "Register a downstream app this platform pushes an organization's people to over SCIM, or every organization's with environment_wide." + description: 'Requires scope `provisioning:write`. Danger: critical.' + operationId: provisioning_targets_create + x-action: provisioning.targets.create + x-scope: 'provisioning:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: 'The organization the target belongs to. Send this or environment_wide.' + maxLength: 64 + environment_wide: + type: boolean + description: "True to make the target the environment's own rather than one organization's. Send this or organization_id." + name: + type: string + description: 'What administrators call it: "Slack", "Salesforce".' + maxLength: 190 + base_url: + type: string + description: "The target's SCIM base URL. Must be a public address." + format: uri + maxLength: 500 + auth_scheme: + type: string + description: 'bearer (a long-lived token the app issued you) or oauth2_client_credentials (a token fetched before each batch).' + enum: + - bearer + - oauth2_client_credentials + secret: + type: string + description: 'The bearer token, or the OAuth client secret. Write-only.' + maxLength: 4096 + token_url: + type: + - string + - 'null' + description: 'oauth2_client_credentials: the token endpoint.' + format: uri + maxLength: 2048 + client_id: + type: + - string + - 'null' + description: 'oauth2_client_credentials: the client id.' + maxLength: 400 + scope: + type: + - string + - 'null' + description: 'oauth2_client_credentials: the scope to ask for, if the app wants one.' + maxLength: 400 + required: + - name + - base_url + - auth_scheme + - secret + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ProvisioningTarget' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Provisioning + summary: 'List the downstream SCIM targets people are provisioned to, optionally for one organization, with their failures. Never their credentials.' + description: 'Requires scope `provisioning:read`. Danger: read.' + operationId: provisioning_targets_list + x-action: provisioning.targets.list + x-scope: 'provisioning:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only this organization's." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/ProvisioningTarget' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/provisioning-targets/{id}': + delete: + tags: + - Provisioning + summary: 'Delete a downstream SCIM target. Nothing more is pushed to it.' + description: 'Requires scope `provisioning:write`. Danger: destructive.' + operationId: provisioning_targets_delete + x-action: provisioning.targets.delete + x-scope: 'provisioning:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The target's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Provisioning + summary: 'Read one downstream SCIM target: its URL, auth scheme, status and last error. Never its credential.' + description: 'Requires scope `provisioning:read`. Danger: read.' + operationId: provisioning_targets_get + x-action: provisioning.targets.get + x-scope: 'provisioning:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The target's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ProvisioningTarget' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/provisioning-targets/{id}/status': + post: + tags: + - Provisioning + summary: 'Pause or resume pushing people to a downstream SCIM target.' + description: 'Requires scope `provisioning:write`. Danger: write.' + operationId: provisioning_targets_status_set + x-action: provisioning.targets.status.set + x-scope: 'provisioning:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The target's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: 'true to resume pushing, false to pause.' + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + required: + - active + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ProvisioningTarget' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /roles: + get: + operationId: roles_list + x-action: roles.list + tags: + - Roles + summary: 'List roles' + description: | + Requires scope `roles:read`. Every role that can still be granted, ordered by name, + with the permissions each carries. Not paginated. `?client_id=` narrows to one + app's roles; `?organization_id=` to what can be granted in that organization. + parameters: + - + name: client_id + in: query + required: false + schema: + type: string + - + name: organization_id + in: query + required: false + schema: + type: string + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/Role' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Roles + summary: 'Define a role for one organization, or (organization_id null) for the whole environment, optionally scoped to one app, with its opening permissions.' + description: 'Requires scope `role_definitions:write`. Danger: write.' + operationId: roles_create + x-action: roles.create + x-scope: 'role_definitions:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + description: + type: + - string + - 'null' + maxLength: 500 + organization_id: + type: + - string + - 'null' + description: 'The organization it belongs to; null defines it for every organization in the environment.' + maxLength: 64 + client_id: + type: + - string + - 'null' + description: 'The app whose tokens it is stamped into; null for every app.' + maxLength: 190 + permissions: + type: array + description: 'Permission ids to grant it from the start.' + maxItems: 200 + items: + type: string + maxLength: 64 + required: + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Role' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/roles/{id}': + delete: + tags: + - Roles + summary: "Delete a role: everyone who holds it loses it. An app-declared role is removed from the app's manifest instead." + description: 'Requires scope `role_definitions:write`. Danger: destructive.' + operationId: roles_delete + x-action: roles.delete + x-scope: 'role_definitions:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The role id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Roles + summary: 'Get one role and the permissions it carries. Name it by id, or by manifest `key` with `client_id`.' + description: 'Requires scope `roles:read`. Danger: read.' + operationId: roles_get + x-action: roles.get + x-scope: 'roles:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The role id, or its manifest `key` with `client_id`.' + maxLength: 190 + - + name: client_id + in: query + required: false + schema: + type: string + description: 'The app whose manifest `key` `id` is.' + maxLength: 255 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Role' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Roles + summary: "Rename a role or change its description. An app-declared role is changed in the app's manifest instead." + description: 'Requires scope `role_definitions:write`. Danger: write.' + operationId: roles_update + x-action: roles.update + x-scope: 'role_definitions:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The role id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 120 + description: + type: + - string + - 'null' + maxLength: 500 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Role' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/roles/{id}/permissions/{permission_id}': + put: + tags: + - Roles + summary: "Add a permission to a role: everyone holding the role gains it. The permission must be unscoped or declared by the role's own app." + description: 'Requires scope `role_definitions:write`. Danger: write.' + operationId: roles_permissions_grant + x-action: roles.permissions.grant + x-scope: 'role_definitions:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The role id.' + maxLength: 64 + - + name: permission_id + in: path + required: true + schema: + type: string + description: 'The permission id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Role' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + tags: + - Roles + summary: 'Remove a permission from a role: everyone holding the role loses it.' + description: 'Requires scope `role_definitions:write`. Danger: destructive.' + operationId: roles_permissions_revoke + x-action: roles.permissions.revoke + x-scope: 'role_definitions:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The role id.' + maxLength: 64 + - + name: permission_id + in: path + required: true + schema: + type: string + description: 'The permission id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Role' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /saml-apps: + post: + tags: + - 'SAML apps' + summary: 'Register a SAML app that signs people in with their account here. Decides where assertions — and their attributes — are sent.' + description: 'Requires scope `saml_apps:write`. Danger: critical.' + operationId: saml_apps_create + x-action: saml_apps.create + x-scope: 'saml_apps:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + entity_id: + type: string + description: "The application's SAML EntityID, unique in this environment." + maxLength: 500 + acs_url: + type: string + description: 'Where assertions are posted: https (http only on localhost), absolute, no fragment.' + format: uri + maxLength: 1000 + name_id_format: + type: string + description: 'The NameID format URN. Default emailAddress.' + enum: + - 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + - 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' + - 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + - 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' + name_id_attribute: + type: string + description: "The person's field the NameID is read from. Default `email`." + maxLength: 120 + attribute_mappings: + type: array + description: 'The attributes the assertion carries. Sent, it is the COMPLETE list afterwards.' + maxItems: 100 + items: + type: object + properties: + attribute: + type: string + description: 'The attribute name the assertion emits: `email`, `displayName`.' + maxLength: 190 + field: + type: string + description: "The person's field it is read from: `email`, `name`." + maxLength: 190 + required: + - attribute + - field + want_authn_requests_signed: + type: boolean + description: "Verify the application's signature on every AuthnRequest. Needs a certificate." + certificate: + type: + - string + - 'null' + description: "The application's signing certificate (PEM). Write-only; left out or null keeps the one on file." + maxLength: 20000 + organization_id: + type: + - string + - 'null' + description: 'The organization that owns the application: only its active members are signed in to it, and the assertion names it. Null makes it environment-wide — every person in the environment may sign in to it.' + maxLength: 64 + required: + - entity_id + - acs_url + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SamlApp' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'SAML apps' + summary: 'List the SAML apps that trust this environment as their identity provider.' + description: 'Requires scope `saml_apps:read`. Danger: read.' + operationId: saml_apps_list + x-action: saml_apps.list + x-scope: 'saml_apps:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/SamlApp' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/saml-apps/{id}': + delete: + tags: + - 'SAML apps' + summary: 'Remove a SAML app. People can no longer sign in to it with their account here.' + description: 'Requires scope `saml_apps:write`. Danger: critical.' + operationId: saml_apps_delete + x-action: saml_apps.delete + x-scope: 'saml_apps:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The SAML app's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'SAML apps' + summary: 'Read one SAML app: its entity id, ACS URL, NameID and attribute mappings. Never its certificate.' + description: 'Requires scope `saml_apps:read`. Danger: read.' + operationId: saml_apps_get + x-action: saml_apps.get + x-scope: 'saml_apps:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The SAML app's id." + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SamlApp' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - 'SAML apps' + summary: "Change a SAML app's entity id, ACS URL, NameID, attribute mappings, signing certificate or owning organization." + description: 'Requires scope `saml_apps:write`. Danger: critical.' + operationId: saml_apps_update + x-action: saml_apps.update + x-scope: 'saml_apps:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The SAML app's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + entity_id: + type: string + description: "The application's SAML EntityID, unique in this environment." + maxLength: 500 + acs_url: + type: string + description: 'Where assertions are posted: https (http only on localhost), absolute, no fragment.' + format: uri + maxLength: 1000 + name_id_format: + type: string + description: 'The NameID format URN. Default emailAddress.' + enum: + - 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + - 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' + - 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + - 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' + name_id_attribute: + type: string + description: "The person's field the NameID is read from. Default `email`." + maxLength: 120 + attribute_mappings: + type: array + description: 'The attributes the assertion carries. Sent, it is the COMPLETE list afterwards.' + maxItems: 100 + items: + type: object + properties: + attribute: + type: string + description: 'The attribute name the assertion emits: `email`, `displayName`.' + maxLength: 190 + field: + type: string + description: "The person's field it is read from: `email`, `name`." + maxLength: 190 + required: + - attribute + - field + want_authn_requests_signed: + type: boolean + description: "Verify the application's signature on every AuthnRequest. Needs a certificate." + certificate: + type: + - string + - 'null' + description: "The application's signing certificate (PEM). Write-only; left out or null keeps the one on file." + maxLength: 20000 + organization_id: + type: + - string + - 'null' + description: 'The organization that owns the application: only its active members are signed in to it, and the assertion names it. Null makes it environment-wide — every person in the environment may sign in to it.' + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SamlApp' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sign-in/policy: + get: + tags: + - Sign-in + summary: "Read the authentication policy (password, MFA, SSO, lockout): the environment baseline, or one organization's effective rules and override." + description: 'Requires scope `signin:read`. Danger: read.' + operationId: signin_policy_get + x-action: signin.policy.get + x-scope: 'signin:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Read one organization's rules. Left out, the environment baseline." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SignInPolicy' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Sign-in + summary: "Change the authentication policy of the environment baseline, or tighten one organization's override. Requiring SSO signs out password sessions." + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: signin_policy_update + x-action: signin.policy.update + x-scope: 'signin:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Write this organization's override. Left out, the environment baseline." + maxLength: 64 + min_length: + type: integer + description: 'Minimum password length.' + minimum: 8 + maximum: 128 + require_breach_check: + type: boolean + description: 'Refuse passwords that appear in known breaches.' + max_age_days: + type: + - integer + - 'null' + description: 'Days before a password must be changed; null for no limit.' + minimum: 1 + maximum: 3650 + reuse_history: + type: integer + description: 'How many previous passwords may not be reused.' + minimum: 0 + maximum: 24 + mfa: + type: string + description: 'Whether a second factor is off, optional or required.' + enum: + - 'off' + - optional + - required + sso: + type: string + description: 'Whether SSO is off, preferred, or required — `required` refuses every other way in and signs out password sessions.' + enum: + - 'off' + - preferred + - required + lockout_threshold: + type: + - integer + - 'null' + description: 'Failed attempts before an account is locked; null for the deployment default (10 inside 15 minutes unless CBOX_ID_LOCKOUT_THRESHOLD says otherwise).' + minimum: 3 + maximum: 100 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SignInPolicy' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sign-in/policy/organizations/{organization_id}': + delete: + tags: + - Sign-in + summary: "Drop one organization's authentication policy override, so it inherits the environment baseline again." + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: signin_policy_inherit + x-action: signin.policy.inherit + x-scope: 'signin:write' + x-danger: critical + parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization whose override to drop.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sign-in/self-service-signup: + put: + tags: + - Sign-in + summary: 'Switch self-service sign-up on or off: whether people can create an account and their own organization in this environment.' + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: signin_self_service_signup_set + x-action: signin.self_service_signup.set + x-scope: 'signin:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + description: 'On: anyone may create an account and their own organization. Off: people join by invitation.' + required: + - enabled + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sign-in/social-providers: + get: + tags: + - Sign-in + summary: 'List the social login providers (Google, GitHub, Apple…) enabled in this environment, optionally for one organization.' + description: 'Requires scope `signin:read`. Danger: read.' + operationId: signin_social_list + x-action: signin.social.list + x-scope: 'signin:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: string + description: "Only this organization's providers." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/SocialProvider' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Sign-in + summary: 'Enable a social login provider (Google, GitHub, Apple…) for one organization with its client credentials. The secret is never returned.' + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: signin_social_set + x-action: signin.social.set + x-scope: 'signin:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: string + description: 'The organization whose sign-in page offers it.' + maxLength: 64 + provider: + type: string + description: 'The catalogue key: google, microsoft, okta, auth0, keycloak, gitlab, slack, github, discord, apple, facebook.' + maxLength: 100 + client_id: + type: string + description: 'The client id from your own account with the provider. For Apple, the Services ID.' + maxLength: 400 + client_secret: + type: + - string + - 'null' + description: 'The client secret. Write-only. Not used by Apple, which signs its own.' + maxLength: 5000 + parameters: + type: object + description: "What the chosen provider needs besides its client credentials — Microsoft's directory, Okta's domain, Apple's team id, key id and private key." + properties: + directory: + type: + - string + - 'null' + description: 'Directory (tenant) ID (Microsoft Entra ID).' + maxLength: 5000 + domain: + type: + - string + - 'null' + description: 'Okta domain (Okta).' + maxLength: 5000 + host: + type: + - string + - 'null' + description: 'Keycloak host (Keycloak).' + maxLength: 5000 + key_id: + type: + - string + - 'null' + description: 'Key ID (Apple).' + maxLength: 5000 + private_key: + type: + - string + - 'null' + description: 'Private key (.p8) (Apple).' + maxLength: 5000 + realm: + type: + - string + - 'null' + description: 'Realm (Keycloak).' + maxLength: 5000 + team_id: + type: + - string + - 'null' + description: 'Team ID (Apple).' + maxLength: 5000 + required: + - organization_id + - provider + - client_id + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SocialProvider' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sign-in/social-providers/{id}': + delete: + tags: + - Sign-in + summary: 'Stop offering a social login provider. People who used it keep their accounts.' + description: 'Requires scope `signin:write`. Danger: critical.' + operationId: signin_social_delete + x-action: signin.social.delete + x-scope: 'signin:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The provider's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only remove it if it is this organization's." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sod-policies: + post: + tags: + - Governance + summary: 'Define a role-conflict rule: two or more roles no one person may hold together. Enforced on every grant from now on.' + description: 'Requires scope `governance:write`. Danger: write.' + operationId: sod_policies_create + x-action: sod_policies.create + x-scope: 'governance:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: 'The organization the rule belongs to. Send this or environment_wide.' + maxLength: 64 + environment_wide: + type: boolean + description: "True to make the rule the environment's own rather than one organization's. Send this or organization_id." + name: + type: string + description: 'What the rule is for: "Payments maker/checker".' + maxLength: 190 + description: + type: + - string + - 'null' + description: 'Why the roles conflict.' + maxLength: 500 + role_ids: + type: array + description: 'The roles no one person may hold together; at least two.' + minItems: 2 + maxItems: 50 + items: + type: string + maxLength: 64 + required: + - name + - role_ids + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SodPolicy' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Governance + summary: 'List role-conflict (segregation of duties) rules: sets of roles no one person may hold together. Optionally those binding one organization.' + description: 'Requires scope `governance:read`. Danger: read.' + operationId: sod_policies_list + x-action: sod_policies.list + x-scope: 'governance:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: 'Only the rules binding this organization: its own and the environment-wide ones.' + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/SodPolicy' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sod-policies/{id}': + delete: + tags: + - Governance + summary: 'Remove a role-conflict rule. Grants it refused are allowed from then on.' + description: 'Requires scope `governance:write`. Danger: critical.' + operationId: sod_policies_delete + x-action: sod_policies.delete + x-scope: 'governance:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The rule's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Governance + summary: 'Read one role-conflict rule: the roles no one person may hold together, and whether it is enforced.' + description: 'Requires scope `governance:read`. Danger: read.' + operationId: sod_policies_get + x-action: sod_policies.get + x-scope: 'governance:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The rule's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: 'Only if it binds this organization (its own, or environment-wide); anything else is a 404.' + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SodPolicy' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sod-policies/{id}/status': + post: + tags: + - Governance + summary: 'Enforce a role-conflict rule, or stop enforcing it — switched off, grants that break it are allowed again.' + description: 'Requires scope `governance:write`. Danger: critical.' + operationId: sod_policies_status_set + x-action: sod_policies.status.set + x-scope: 'governance:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The rule's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: 'true to enforce the rule, false to stop enforcing it.' + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + required: + - active + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SodPolicy' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sso/connections: + post: + tags: + - 'Enterprise SSO' + summary: "Connect an organization's SAML or OIDC identity provider as a draft. Activate it to start signing people in through it." + description: 'Requires scope `sso:write`. Danger: write.' + operationId: sso_connections_create + x-action: sso.connections.create + x-scope: 'sso:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: 'The organization the connection belongs to. Send this or environment_wide.' + maxLength: 64 + environment_wide: + type: boolean + description: "True to make the connection the environment's own rather than one organization's. Send this or organization_id." + name: + type: string + description: 'What administrators call it: "Okta", "Entra ID".' + maxLength: 120 + type: + type: string + description: 'saml or oidc. The config fields of that type are required, unless pending_idp.' + enum: + - saml + - oidc + pending_idp: + type: boolean + description: "True to create the draft before the identity provider's details are known: the answer's service_provider is what to paste into it. Complete it with update before activating." + idp_entity_id: + type: + - string + - 'null' + description: "SAML: the identity provider's entity id." + maxLength: 500 + idp_sso_url: + type: + - string + - 'null' + description: "SAML: the identity provider's single sign-on URL." + format: uri + maxLength: 500 + idp_x509cert: + type: + - string + - 'null' + description: "SAML: the identity provider's signing certificate (PEM). Write-only." + maxLength: 20000 + sp_entity_id: + type: + - string + - 'null' + description: "SAML: the entity id this environment presents as the service provider. Left out on create, this connection's own." + maxLength: 500 + sp_acs_url: + type: + - string + - 'null' + description: "SAML: where the identity provider posts its assertions. Left out on create, this connection's own ACS URL." + format: uri + maxLength: 500 + issuer: + type: + - string + - 'null' + description: "OIDC: the provider's issuer URL; its endpoints are discovered from it." + format: uri + maxLength: 500 + client_id: + type: + - string + - 'null' + description: 'OIDC: the client id registered at the provider.' + maxLength: 500 + client_secret: + type: + - string + - 'null' + description: 'OIDC: the client secret. Write-only.' + maxLength: 500 + signing_key: + type: + - string + - 'null' + description: 'OIDC: the signing key. Write-only.' + maxLength: 20000 + required: + - name + - type + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoConnection' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Enterprise SSO' + summary: 'List the SAML and OIDC connections organizations sign in through, optionally for one organization. Never a certificate or secret.' + description: 'Requires scope `sso:read`. Danger: read.' + operationId: sso_connections_list + x-action: sso.connections.list + x-scope: 'sso:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only this organization's." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/SsoConnection' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/connections/{id}': + delete: + tags: + - 'Enterprise SSO' + summary: 'Delete an SSO connection and its settings. Where SSO is required, its people cannot sign in until another is activated.' + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_delete + x-action: sso.connections.delete + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Enterprise SSO' + summary: 'Read one SSO connection: its type, status, entity ids, URLs, issuer and client id. Never a certificate or secret.' + description: 'Requires scope `sso:read`. Danger: read.' + operationId: sso_connections_get + x-action: sso.connections.get + x-scope: 'sso:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoConnection' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - 'Enterprise SSO' + summary: "Change an SSO connection's name or identity-provider settings. Secrets left out keep the ones on file." + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_update + x-action: sso.connections.update + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + name: + type: string + description: 'A new name.' + maxLength: 120 + idp_entity_id: + type: + - string + - 'null' + description: "SAML: the identity provider's entity id." + maxLength: 500 + idp_sso_url: + type: + - string + - 'null' + description: "SAML: the identity provider's single sign-on URL." + format: uri + maxLength: 500 + idp_x509cert: + type: + - string + - 'null' + description: "SAML: the identity provider's signing certificate (PEM). Write-only." + maxLength: 20000 + sp_entity_id: + type: + - string + - 'null' + description: "SAML: the entity id this environment presents as the service provider. Left out on create, this connection's own." + maxLength: 500 + sp_acs_url: + type: + - string + - 'null' + description: "SAML: where the identity provider posts its assertions. Left out on create, this connection's own ACS URL." + format: uri + maxLength: 500 + issuer: + type: + - string + - 'null' + description: "OIDC: the provider's issuer URL; its endpoints are discovered from it." + format: uri + maxLength: 500 + client_id: + type: + - string + - 'null' + description: 'OIDC: the client id registered at the provider.' + maxLength: 500 + client_secret: + type: + - string + - 'null' + description: 'OIDC: the client secret. Write-only.' + maxLength: 500 + signing_key: + type: + - string + - 'null' + description: 'OIDC: the signing key. Write-only.' + maxLength: 20000 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoConnection' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/connections/{id}/activate': + post: + tags: + - 'Enterprise SSO' + summary: 'Activate an SSO connection: people whose email domain routes to it start signing in through it.' + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_activate + x-action: sso.connections.activate + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoConnection' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/connections/{id}/certificates': + get: + tags: + - 'Enterprise SSO' + summary: "List a SAML connection's signing certificates — primary and staged — with each one's fingerprint and expiry." + description: 'Requires scope `sso:read`. Danger: read.' + operationId: sso_connections_certificates_list + x-action: sso.connections.certificates.list + x-scope: 'sso:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoCertificates' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - 'Enterprise SSO' + summary: "Stage a SAML connection's new IdP signing certificate (PEM or IdP metadata) beside the current one, so a renewal has no outage. Activate it once the IdP signs with it." + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_certificates_stage + x-action: sso.connections.certificates.stage + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + certificate: + type: + - string + - 'null' + description: 'The new signing certificate, PEM. Send this or metadata.' + maxLength: 20000 + metadata: + type: + - string + - 'null' + description: "The identity provider's metadata XML, or the https URL it is published at. Send this or certificate." + maxLength: 500000 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoCertificates' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/connections/{id}/certificates/activate': + post: + tags: + - 'Enterprise SSO' + summary: "Make a staged SAML signing certificate the connection's primary. The certificate it replaces stops being trusted." + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_certificates_activate + x-action: sso.connections.certificates.activate + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + fingerprint_sha256: + type: string + description: "The staged certificate's SHA-256 fingerprint, as the certificate list gives it." + maxLength: 200 + required: + - fingerprint_sha256 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoCertificates' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/connections/{id}/disable': + post: + tags: + - 'Enterprise SSO' + summary: 'Disable an SSO connection without deleting it. Where SSO is required, its people cannot sign in until it is re-activated.' + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_disable + x-action: sso.connections.disable + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoConnection' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/connections/{id}/require-sso': + post: + tags: + - 'Enterprise SSO' + summary: 'Require single sign-on for the organization an SSO connection belongs to. Ends every password session there.' + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_connections_require_sso + x-action: sso.connections.require_sso + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The connection's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SignInPolicy' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sso/domains: + post: + tags: + - 'Enterprise SSO' + summary: "Claim an email domain for an organization's single sign-on, and get the DNS TXT record to publish to prove it." + description: 'Requires scope `sso:write`. Danger: write.' + operationId: sso_domains_create + x-action: sso.domains.create + x-scope: 'sso:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: string + description: 'The organization claiming it.' + maxLength: 64 + domain: + type: string + description: 'The email domain, e.g. acme.com.' + maxLength: 253 + required: + - organization_id + - domain + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Enterprise SSO' + summary: 'List the email domains organizations claimed for single sign-on, whether each is verified and captured, and the DNS record that proves an unverified one.' + description: 'Requires scope `sso:read`. Danger: read.' + operationId: sso_domains_list + x-action: sso.domains.list + x-scope: 'sso:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "Only this organization's." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/SsoDomain' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/domains/{id}': + delete: + tags: + - 'Enterprise SSO' + summary: "Remove a claimed email domain. Its people stop being routed to the organization's SSO connection." + description: 'Requires scope `sso:write`. Danger: destructive.' + operationId: sso_domains_delete + x-action: sso.domains.delete + x-scope: 'sso:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The domain's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/domains/{id}/capture': + post: + tags: + - 'Enterprise SSO' + summary: "Turn capture on or off for a verified domain: captured, everyone with an address at it must sign in through the organization's SSO." + description: 'Requires scope `sso:write`. Danger: critical.' + operationId: sso_domains_capture + x-action: sso.domains.capture + x-scope: 'sso:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The domain's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + capture: + type: boolean + description: 'true to capture the domain, false to release it.' + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + required: + - capture + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/sso/domains/{id}/verify': + post: + tags: + - 'Enterprise SSO' + summary: "Check a claimed domain's DNS TXT record now. Answers the domain, with verified true once the record is found." + description: 'Requires scope `sso:write`. Danger: write.' + operationId: sso_domains_verify + x-action: sso.domains.verify + x-scope: 'sso:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The domain's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "Only if it is this organization's; anything else is a 404." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SsoDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /sso/saml-metadata: + post: + tags: + - 'Enterprise SSO' + summary: "Parse an identity provider's SAML metadata (XML or a metadata URL) into the entity id, SSO URL and certificate a SAML connection needs. Stores nothing." + description: 'Requires scope `sso:write`. Danger: write.' + operationId: sso_saml_metadata_import + x-action: sso.saml_metadata.import + x-scope: 'sso:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + metadata: + type: string + description: 'The IdP metadata XML, or an https URL it is published at.' + maxLength: 500000 + required: + - metadata + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SamlMetadata' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /support-sessions: + post: + operationId: support_sessions_start + x-action: support_sessions.start + tags: + - 'Support sessions' + summary: 'Start a support session' + description: | + Requires scope `support:write`. A member of your staff (`actor_user_id`) signs in to + one of your apps (`client_id`) AS one of a customer's users (`user_id` in + `organization_id`), for a stated `reason`, for at most 60 minutes. + + The actor must hold your app's own `support:impersonate` permission through an + **environment-wide** grant (see `PUT /users/{id}/environment-roles/{role_id}`); + otherwise `403 not_permitted`. The app must be a first-party app the environment + owns that uses the authorization-code grant (`422 client_not_eligible`). The target + must be an active member (`422 target_not_member`) of an active organization + (`409 organization_inactive`). + + Send `redirect_uri` (registered for the app) and a PKCE S256 `code_challenge` to get + the first authorization `code` in the response. Your app redeems it at `/oauth/token` + with the verifier; every token carries `act: {"sub": actor_user_id}` and there is + never a refresh token. The session is audited on the customer's trail and the + environment's, and announced as the `support_session.started` webhook. + + `scopes` in the response are exactly what every token of the session carries: the + ones asked for (or, with none, the app's registration), never `offline_access`, and + audienced the way the token endpoint audiences them — once any of them belongs to a + registered API, the token is for that API and a scope no API registered (such as + `apps.manifest`) is left out. Scopes of two registered APIs cannot be audienced to + one token and are refused before the session starts (`422 invalid_target`). + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - user_id + - organization_id + - client_id + - actor_user_id + - reason + properties: + user_id: + type: string + description: "The customer's user to act as." + organization_id: + type: string + client_id: + type: string + description: "The app's public client id." + actor_user_id: + type: string + description: 'Your staff member.' + reason: + type: string + maxLength: 500 + description: 'Shown to the customer.' + ttl_minutes: + type: integer + minimum: 1 + maximum: 60 + scopes: + type: array + items: + type: string + description: "Narrowed to the app's registration; omitted means all of it." + redirect_uri: + type: string + code_challenge: + type: string + pattern: '^[A-Za-z0-9_-]{43}$' + nonce: + type: + - string + - 'null' + responses: + 201: + description: Started. + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SupportSession' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/support-sessions/{id}': + delete: + tags: + - 'Support sessions' + summary: 'End a support session now: every token it issued is revoked.' + description: 'Requires scope `support:write`. Danger: destructive.' + operationId: support_sessions_end + x-action: support_sessions.end + x-scope: 'support:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The support session id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /token-vault/secrets: + post: + tags: + - 'Token vault' + summary: "Store a downstream credential (a third-party API key) in an organization's token vault, or the environment's own. The value is sealed and never returned." + description: 'Requires scope `token_vault:write`. Danger: write.' + operationId: token_vault_secrets_create + x-action: token_vault.secrets.create + x-scope: 'token_vault:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + name: + type: string + description: 'What it is: "Stripe live key".' + maxLength: 190 + provider: + type: string + description: 'Whose credential it is: "stripe", "openai".' + maxLength: 190 + secret: + type: string + description: 'The credential. Write-only: sealed, never returned.' + maxLength: 20000 + required: + - name + - provider + - secret + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/TokenVaultSecret' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - 'Token vault' + summary: "List the downstream credentials stored in an organization's token vault (or the environment's own): names, providers, status. Never a value." + description: 'Requires scope `token_vault:read`. Danger: read.' + operationId: token_vault_secrets_list + x-action: token_vault.secrets.list + x-scope: 'token_vault:read' + x-danger: read + parameters: + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/TokenVaultSecret' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/token-vault/secrets/{id}': + get: + tags: + - 'Token vault' + summary: 'Read one stored credential and the client ids granted to lease it. Never its value.' + description: 'Requires scope `token_vault:read`. Danger: read.' + operationId: token_vault_secrets_get + x-action: token_vault.secrets.get + x-scope: 'token_vault:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The secret's id." + - + name: organization_id + in: query + required: false + schema: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/TokenVaultSecret' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/token-vault/secrets/{id}/grants': + post: + tags: + - 'Token vault' + summary: 'Grant an app (by OAuth client id) the right to lease a stored credential.' + description: 'Requires scope `token_vault:write`. Danger: critical.' + operationId: token_vault_grants_create + x-action: token_vault.grants.create + x-scope: 'token_vault:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The secret's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + client_id: + type: string + description: 'The OAuth client id of the app that may lease it.' + maxLength: 190 + required: + - client_id + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/TokenVaultSecret' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/token-vault/secrets/{id}/grants/{client_id}': + delete: + tags: + - 'Token vault' + summary: "Withdraw an app's right to lease a stored credential." + description: 'Requires scope `token_vault:write`. Danger: destructive.' + operationId: token_vault_grants_delete + x-action: token_vault.grants.delete + x-scope: 'token_vault:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The secret's id." + - + name: client_id + in: path + required: true + schema: + type: string + description: 'The OAuth client id whose grant is withdrawn.' + maxLength: 190 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/token-vault/secrets/{id}/revoke': + post: + tags: + - 'Token vault' + summary: 'Revoke a stored credential permanently. No app can lease it again.' + description: 'Requires scope `token_vault:write`. Danger: destructive.' + operationId: token_vault_secrets_revoke + x-action: token_vault.secrets.revoke + x-scope: 'token_vault:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The secret's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/TokenVaultSecret' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/token-vault/secrets/{id}/rotate': + post: + tags: + - 'Token vault' + summary: "Replace a stored credential's value. Every later lease hands out the new one. The value is never returned." + description: 'Requires scope `token_vault:write`. Danger: critical.' + operationId: token_vault_secrets_rotate + x-action: token_vault.secrets.rotate + x-scope: 'token_vault:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The secret's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_id: + type: + - string + - 'null' + description: "The organization whose vault this is. Left out: the environment's own secrets, a separate collection." + maxLength: 64 + secret: + type: string + description: 'The new value. Write-only: sealed, never returned.' + maxLength: 20000 + required: + - secret + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/TokenVaultSecret' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /users: + get: + operationId: users_list + x-action: users.list + tags: + - Users + summary: 'List users' + description: | + Requires scope `users:read`. A page at a time, in id order. Narrow it the ways you + look for somebody: `email` (exactly that address, case-insensitive), `q` (a fragment + of the address or the name) and `status`. Filters combine. + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/After' + - + name: email + in: query + required: false + schema: + type: string + format: email + maxLength: 190 + - + name: q + in: query + required: false + description: 'A fragment of the address or the name.' + schema: + type: string + maxLength: 190 + - + name: status + in: query + required: false + schema: + type: string + enum: + - active + - disabled + - locked + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 400: + $ref: '#/components/responses/EnvironmentRequired' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + operationId: users_create + x-action: users.create + tags: + - Users + summary: 'Create a user' + description: 'Requires scope `users:write`. Password is optional — omit it to create a passwordless identity the user completes via an invite/magic-link.' + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + format: email + maxLength: 190 + name: + type: + - string + - 'null' + maxLength: 190 + password: + type: + - string + - 'null' + minLength: 8 + maxLength: 255 + description: "Checked against this environment's password policy (`422 password_policy`)." + send_sign_in_link: + type: boolean + default: false + description: 'Mail the new user a one-click sign-in link.' + responses: + 201: + description: Created + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}': + get: + operationId: users_get + x-action: users.get + tags: + - Users + summary: 'Get a user' + description: 'Requires scope `users:read`.' + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + operationId: users_deactivate + x-action: users.deactivate + tags: + - Users + summary: 'Deactivate a user' + description: | + Requires scope `users:write`. A **soft disable** — the identity can no longer + authenticate, but is never hard-deleted — and every OAuth grant they hold is + revoked, which reactivating them does not bring back. Returns the user in its new + `disabled` state. Idempotent. The console's "Deactivate" is this action. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Users + summary: "Change a user's name and/or email address. A changed address must be verified again." + description: 'Requires scope `users:write`. Danger: critical.' + operationId: users_update + x-action: users.update + x-scope: 'users:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + name: + type: + - string + - 'null' + maxLength: 190 + email: + type: string + description: 'The new address. Its verification is cleared.' + format: email + maxLength: 190 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/environment-roles': + get: + operationId: users_environment_roles_list + x-action: users.environment_roles.list + tags: + - Roles + summary: 'List the roles a user holds everywhere (staff)' + description: 'Requires scope `roles:read`.' + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/RoleAssignment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/environment-roles/{role_id}': + get: + operationId: users_environment_roles_get + x-action: users.environment_roles.get + tags: + - Roles + summary: 'Whether a user holds a role everywhere' + description: 'Requires scope `roles:read`. `200` with the grant, or `404` when it is not held.' + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/RoleId' + - $ref: '#/components/parameters/RoleClientId' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: Held. + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/RoleAssignment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + put: + operationId: users_environment_roles_grant + x-action: users.environment_roles.grant + tags: + - Roles + summary: 'Grant a role everywhere in the environment (staff)' + description: | + Requires scope `roles:write`. Idempotent. For your own people who act across every + customer. Any role that belongs to no organization may be granted this way: an + app-agnostic one reaches every app's tokens, one your app declared reaches only + your app's. An organization's own role is refused with `422 role_not_assignable`; a + grant that would form a segregation-of-duties conflict in any organization the + person belongs to with `409 role_conflict`. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/RoleId' + - $ref: '#/components/parameters/RoleClientId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: 'Granted (or already held).' + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/RoleAssignment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + operationId: users_environment_roles_revoke + x-action: users.environment_roles.revoke + tags: + - Roles + summary: 'Take back an environment-wide grant' + description: 'Requires scope `roles:write`. Idempotent.' + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/RoleId' + - $ref: '#/components/parameters/RoleClientId' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: 'Not held any more.' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/erase': + post: + tags: + - Users + summary: 'Erase a person (GDPR Art. 17): revoke their sessions and tokens, delete their credentials, memberships and personal data, and pseudonymise their account. Cannot be undone.' + description: 'Requires scope `users:erase`. Danger: critical.' + operationId: users_erase + x-action: users.erase + x-scope: 'users:erase' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: "The user's id." + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ErasureReceipt' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/mfa': + delete: + tags: + - Users + summary: "Reset a user's two-factor authentication: their authenticator and recovery codes are removed and they must enrol again." + description: 'Requires scope `users:write`. Danger: critical.' + operationId: users_mfa_reset + x-action: users.mfa.reset + x-scope: 'users:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/password': + post: + tags: + - Users + summary: "Set a user's password, temporary by default, and mail it to them unless told not to. Signs them out according to `revoke`." + description: 'Requires scope `users:write`. Danger: critical.' + operationId: users_password_set + x-action: users.password.set + x-scope: 'users:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + password: + type: string + description: "The new password. Checked against this environment's password policy." + maxLength: 200 + reason: + type: string + description: 'Why — recorded on the audit trail.' + maxLength: 200 + temporary: + type: boolean + description: 'The user must choose a new password at their next sign-in. Default true.' + expires_in_hours: + type: integer + description: 'A temporary password stops working after this many hours. 0 or absent: until changed.' + minimum: 0 + maximum: 8760 + revoke: + type: string + description: 'How much existing access to end. Default `sessions_and_tokens`.' + enum: + - sessions_and_tokens + - sessions_only + - nothing + send_email: + type: boolean + description: 'Mail the password to the user. Default true.' + required: + - password + - reason + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/password-reset': + post: + tags: + - Users + summary: 'Mail a user a password-reset link at their own address.' + description: 'Requires scope `users:write`. Danger: write.' + operationId: users_password_reset_send + x-action: users.password_reset.send + x-scope: 'users:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/reactivate': + post: + tags: + - Users + summary: 'Reactivate a deactivated user so they can sign in again. Grants revoked on deactivation stay revoked.' + description: 'Requires scope `users:write`. Danger: write.' + operationId: users_reactivate + x-action: users.reactivate + x-scope: 'users:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/sessions': + get: + operationId: users_sessions_list + x-action: users.sessions.list + tags: + - Users + summary: "List a user's live sign-in sessions" + description: | + Requires scope `users:read`. The fifty most recently active, newest first — enough to + recognise a device and end one (`DELETE /users/{id}/sessions/{session_id}`), not a log. + `impersonation` marks a session somebody else opened as this person. + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserSession' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + delete: + tags: + - Users + summary: 'Sign a user out everywhere: end every session and revoke every OAuth grant they hold.' + description: 'Requires scope `users:write`. Danger: destructive.' + operationId: users_sessions_revoke_all + x-action: users.sessions.revoke_all + x-scope: 'users:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/sessions/{session_id}': + delete: + tags: + - Users + summary: "End one of a user's sign-in sessions." + description: 'Requires scope `users:write`. Danger: destructive.' + operationId: users_sessions_revoke + x-action: users.sessions.revoke + x-scope: 'users:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - + name: session_id + in: path + required: true + schema: + type: string + description: 'The session id, from `GET /users/{id}/sessions`.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/verification': + post: + tags: + - Users + summary: 'Mail a user the link that verifies their email address. Does nothing for an address already verified.' + description: 'Requires scope `users:write`. Danger: write.' + operationId: users_verification_send + x-action: users.verification.send + x-scope: 'users:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/users/{id}/verify': + post: + tags: + - Users + summary: "Mark a user's email address verified without the emailed link. It can then be used to recover the account." + description: 'Requires scope `users:write`. Danger: critical.' + operationId: users_verify + x-action: users.verify + x-scope: 'users:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The user id.' + maxLength: 64 + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/User' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /vault/secrets: + post: + tags: + - 'Token Vault' + summary: 'Store a downstream credential, sealed at rest' + description: | + The secret is sealed with the environment's crypto key and is never returned by + any read endpoint — only a `lease` reveals it, and only to a client holding a + live grant. + + Ownership is derived from the calling token's `org` claim and CANNOT be supplied + in the body: the owner is the vault's tenancy boundary, and a caller-chosen owner + would be no boundary at all. + security: + - + VaultManagerToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + - provider + - secret + properties: + name: + type: string + maxLength: 200 + description: 'Unique per owner — two organizations may each hold a `github-token`.' + provider: + type: string + maxLength: 100 + secret: + type: string + description: 'The value to seal. Never returned by a read.' + expires_at: + type: + - string + - 'null' + format: date-time + responses: + 201: + description: Stored. + content: + application/json: + schema: + $ref: '#/components/schemas/VaultSecret' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/vault/secrets/{id}': + delete: + tags: + - 'Token Vault' + summary: 'Revoke a secret permanently' + description: 'No future lease can open it. Existing leases are not recalled — they are short-lived by design.' + security: + - + VaultManagerToken: [] + parameters: + - $ref: '#/components/parameters/Id' + responses: + 204: + description: Revoked. + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 404: + description: "No such secret in the caller's organization." + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 429: + $ref: '#/components/responses/TooManyRequests' + '/vault/secrets/{id}/grants': + post: + tags: + - 'Token Vault' + summary: 'Authorize an agent client to lease this secret' + security: + - + VaultManagerToken: [] + parameters: + - $ref: '#/components/parameters/Id' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - client_id + properties: + client_id: + type: string + maxLength: 200 + max_ttl_seconds: + type: + - integer + - 'null' + minimum: 1 + responses: + 201: + description: Granted. + content: + application/json: + schema: + type: object + properties: + secret_id: + type: string + client_id: + type: string + max_ttl_seconds: + type: + - integer + - 'null' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 404: + description: "No such secret in the caller's organization." + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/vault/secrets/{id}/grants/{clientId}': + delete: + tags: + - 'Token Vault' + summary: "Revoke an agent's authorization" + description: 'Idempotent — revoking an absent grant succeeds.' + security: + - + VaultManagerToken: [] + parameters: + - $ref: '#/components/parameters/Id' + - + name: clientId + in: path + required: true + schema: + type: string + responses: + 204: + description: 'Revoked (or already absent).' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 429: + $ref: '#/components/responses/TooManyRequests' + '/vault/secrets/{id}/lease': + post: + tags: + - 'Token Vault' + summary: 'Redeem a grant for the plaintext secret' + description: | + The only endpoint that reveals a secret. Every refusal — unknown secret, revoked, + expired, or no live grant — is the SAME 403 with no detail, so a caller cannot + use it to enumerate what exists. + security: + - + AgentToken: [] + parameters: + - $ref: '#/components/parameters/Id' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - purpose + properties: + purpose: + type: string + maxLength: 200 + description: 'Recorded on the audit trail.' + responses: + 200: + description: Leased. + content: + application/json: + schema: + type: object + required: + - secret_id + - provider + - secret + - expires_at + properties: + secret_id: + type: string + provider: + type: string + description: 'The downstream provider the secret belongs to.' + secret: + type: string + expires_at: + type: string + format: date-time + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + description: | + Lease denied. Deliberately uniform — unknown secret, revoked, expired and + ungranted are ONE response, with a constant message, so it cannot be used to + enumerate. A token missing the `vault.lease` scope is an RFC 6750 challenge + instead, and is distinguishable only because it never reaches the vault. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Error' + - $ref: '#/components/schemas/BearerChallenge' + example: + error: lease_denied + message: 'The lease was denied.' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/vault/secrets/{id}/rotate': + post: + tags: + - 'Token Vault' + summary: 'Replace the sealed value, keeping the id and its grants' + security: + - + VaultManagerToken: [] + parameters: + - $ref: '#/components/parameters/Id' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - secret + properties: + secret: + type: string + responses: + 200: + description: Rotated. + content: + application/json: + schema: + $ref: '#/components/schemas/VaultSecret' + 401: + $ref: '#/components/responses/BearerUnauthorized' + 403: + $ref: '#/components/responses/BearerForbidden' + 404: + description: | + No such secret **in the caller's organization**. A secret owned by another + organization is indistinguishable from one that does not exist — the lookup + is filtered, not fetched-then-compared, so nothing leaks about what exists + outside the caller's own scope. The message is a constant for the same reason. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: not_found + message: 'No such secret.' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /webhooks: + post: + tags: + - Webhooks + summary: 'Register a webhook endpoint for one organization or the whole environment. Returns its signing secret once.' + description: 'Requires scope `webhooks:write`. Danger: critical.' + operationId: webhooks_create + x-action: webhooks.create + x-scope: 'webhooks:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + url: + type: string + description: 'A public HTTPS URL. Signed deliveries are POSTed here.' + format: uri + maxLength: 500 + event_types: + type: array + description: 'The events it receives.' + minItems: 1 + items: + type: string + enum: + - user.created + - user.updated + - user.deactivated + - user.login + - user.reactivated + - identity.linked + - user.erased + - organization.created + - organization.suspended + - organization.reactivated + - organization.updated + - organization.deleted + - membership.created + - membership.updated + - membership.deleted + - invitation.created + - invitation.accepted + - invitation.revoked + - role.assigned + - role.unassigned + - role.assigned_everywhere + - role.unassigned_everywhere + - api_key.created + - api_key.revoked + - support_session.started + - directory.user.provisioned + - directory.user.deprovisioned + - directory.user.deactivated + - directory.group.membership_changed + - domain.added + - domain.removed + - domain.verified + - connection.activated + - connection.certificate_expiring + - entitlement.set + - entitlement.updated + - entitlement.revoked + - vault.grant.created + - vault.grant.revoked + - vault.secret.revoked + - governance.access.revoked + organization_id: + type: + - string + - 'null' + description: "The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide." + maxLength: 64 + environment_wide: + type: boolean + description: "True to make it the environment's own, carrying EVERY organization's traffic. Send this or organization_id." + required: + - url + - event_types + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Webhooks + summary: 'List the webhook endpoints registered in this environment: URL, owner, subscribed events and whether each is active.' + description: 'Requires scope `webhooks:read`. Danger: read.' + operationId: webhooks_list + x-action: webhooks.list + x-scope: 'webhooks:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: after + in: query + required: false + schema: + type: string + description: 'The `next_cursor` of the previous page.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/Webhook' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/webhooks/{id}': + delete: + tags: + - Webhooks + summary: 'Delete a webhook endpoint. It stops receiving events at once.' + description: 'Requires scope `webhooks:write`. Danger: destructive.' + operationId: webhooks_delete + x-action: webhooks.delete + x-scope: 'webhooks:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Webhooks + summary: 'Get one webhook endpoint: URL, owner, subscribed events, health. Never its signing secret.' + description: 'Requires scope `webhooks:read`. Danger: read.' + operationId: webhooks_get + x-action: webhooks.get + x-scope: 'webhooks:read' + x-danger: read + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Webhooks + summary: "Change a webhook endpoint's URL and/or the events it subscribes to." + description: 'Requires scope `webhooks:write`. Danger: write.' + operationId: webhooks_update + x-action: webhooks.update + x-scope: 'webhooks:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + url: + type: string + description: 'A public HTTPS URL. Left out, unchanged.' + format: uri + maxLength: 500 + event_types: + type: array + description: 'The COMPLETE set of events afterwards. Left out, unchanged.' + minItems: 1 + items: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/webhooks/{id}/pause': + post: + tags: + - Webhooks + summary: 'Pause a webhook endpoint: it stops receiving events until resumed.' + description: 'Requires scope `webhooks:write`. Danger: write.' + operationId: webhooks_pause + x-action: webhooks.pause + x-scope: 'webhooks:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/webhooks/{id}/resume': + post: + tags: + - Webhooks + summary: 'Resume a paused webhook endpoint: it receives events again.' + description: 'Requires scope `webhooks:write`. Danger: write.' + operationId: webhooks_resume + x-action: webhooks.resume + x-scope: 'webhooks:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/webhooks/{id}/rotate': + post: + tags: + - Webhooks + summary: 'Issue a new signing secret for a webhook endpoint, returned once. The old secret stops verifying immediately.' + description: 'Requires scope `webhooks:write`. Danger: critical.' + operationId: webhooks_secret_rotate + x-action: webhooks.secret.rotate + x-scope: 'webhooks:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' +components: + securitySchemes: + EnvironmentApiKey: + type: http + scheme: bearer + bearerFormat: cbid_env_ + description: | + An environment API key, `cbid_env_…`. Created by the account that owns the environment, + in the console under Identity platform › Environment keys — not in the environment's own + console. Bound to that environment's host, and carries an explicit list of scopes; it may + also carry an expiry. + ManagementAccessToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + A PERSON's OAuth access token, issued by this environment's issuer and audienced to + its management plane (`resource=https:///mcp` — the same token the + MCP server takes). `cbox login` and MCP clients obtain one by signing the person in. + + It may run an operation only when it carries the operation's scope AND the person + could run it on their own console here: an organization administrator, confined to + the organization the token is bound to. Operations that belong to the environment + console do not list it. Every critical operation answers `202 approval_required` + until the person approves it on their device. + + On the organization's own roster (members, their roles, invitations, ownership, its + people's API keys) the organization's own rules hold, as on its People page: an + admin cannot change or remove an owner, nobody removes themselves, only the owner + transfers ownership, a staff-only role is `role_not_assignable`, and a customer + workspace's roster — administered under Workspace › Team — is `409 managed_elsewhere`. A DPoP-bound token is presented as + `Authorization: DPoP ` with a proof. + WorkspaceAccessToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + A workspace member's OAuth access token from the PLATFORM ROOT, audienced to the + root's `/mcp` (`cbox login` at the root; an agent signed in there). Presented on the + ROOT's host, with `Cbox-Environment` naming the environment of the person's + workspace to act in. It runs what that environment's console runs — when the + person's role administers environments and their membership reaches this one — and + only within the scopes it carries. Every critical operation answers + `202 approval_required` until the person approves it on their device. A DPoP-bound + token is presented as `Authorization: DPoP ` with a proof. + AppClientCredentials: + type: http + scheme: basic + description: | + Your APP's own client credentials — `client_id:client_secret` as HTTP Basic + (`client_secret_basic`). `client_secret_post` and `private_key_jwt` are accepted + too, in the body, exactly as at the token endpoint. + VaultManagerToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + An OAuth access token carrying the `vault.manage` scope. NOT an environment API + key: these routes are guarded by `scope:vault.manage`, which introspects an + access token, and a `cbid_env_` key introspects as inactive. Its `org` claim + selects the organization whose secrets it administers. + AgentToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + An OAuth access token carrying the `vault.lease` scope. Its `org` claim selects + the organization whose secrets it may open — the vault's tenancy boundary. + AuthenticatorToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + A DPoP-bound OAuth access token held by the Cbox ID authenticator app, obtained + via authorization_code + PKCE (S256) as a public client. + + Because the token carries a `cnf.jkt`, every request must present it under the + `DPoP` auth scheme together with a matching proof — `htm`, `htu`, a fresh `jti`, + an `iat` within 60 seconds, and an `ath` equal to the SHA-256 of the token + itself. A plain `Bearer` presentation of a bound token is rejected. + parameters: + CboxEnvironment: + name: Cbox-Environment + in: header + required: false + description: | + Only with a `WorkspaceAccessToken`, on the platform root's host: the environment of + your workspace to act in, by id or slug. Required there; ignored with a key, which + is bound to its own environment's host. + schema: + type: string + CboxApproval: + name: Cbox-Approval + in: header + required: false + description: 'The id of an approval this key asked for (from a `202 approval_required`), once the person approved it. Spent by this request, once.' + schema: + type: string + IdempotencyKey: + name: Idempotency-Key + in: header + required: false + description: 'Any unique string (a UUID). A retry with the same key and request gets the first answer back.' + schema: + type: string + maxLength: 255 + ScopeKey: + name: key + in: path + required: true + description: 'The scope key, for example `tax:assess`.' + schema: + type: string + maxLength: 128 + Id: + name: id + in: path + required: true + schema: + type: string + OrganizationId: + name: organization_id + in: path + required: true + description: "The organization's id." + schema: + type: string + UserId: + name: user_id + in: path + required: true + schema: + type: string + InvitationId: + name: invitation_id + in: path + required: true + schema: + type: string + RoleId: + name: role_id + in: path + required: true + description: "The role's id — or its manifest `key`, together with `?client_id=` naming the app that declared it." + schema: + type: string + RoleClientId: + name: client_id + in: query + required: false + description: 'The app whose manifest `key` `{role_id}` is. Not needed when `{role_id}` is an id.' + schema: + type: string + Limit: + name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 50 + After: + name: after + in: query + required: false + description: 'Opaque cursor — the `meta.next_cursor` from the previous page.' + schema: + type: string + schemas: + Device: + type: object + required: + - id + - install_id + - platform + - name + - status + properties: + id: + type: string + install_id: + type: string + platform: + type: string + enum: + - ios + - android + name: + type: string + status: + type: string + enum: + - active + - retired + description: | + Operator/user INTENT, not delivery health. A device can be active and + currently failing, or retired and perfectly reachable. + app_version: + type: + - string + - 'null' + os_version: + type: + - string + - 'null' + model: + type: + - string + - 'null' + last_seen_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + Approval: + type: object + required: + - id + - client_id + - client_name + - scopes + properties: + id: + type: string + description: | + The INTERNAL request id. Never `auth_req_id`, which is the initiating + client's polling secret and is stored only as a SHA-256 hash — a client must + not be able to approve its own request. + client_id: + type: string + client_name: + type: string + binding_message: + type: + - string + - 'null' + description: | + The transaction description the user compares against what the requesting app + showed them. This is the anti-phishing primitive, and the reason approvals are + fetched rather than pushed. + scopes: + type: array + items: + type: object + required: + - scope + - label + properties: + scope: + type: string + label: + type: string + description: 'Human wording, identical to the console.' + organization_id: + type: + - string + - 'null' + requested_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: + - string + - 'null' + format: date-time + ApprovalDecision: + type: object + required: + - id + - status + properties: + id: + type: string + status: + type: string + enum: + - approved + - denied + VaultSecret: + type: object + required: + - id + - name + - provider + - revoked + properties: + id: + type: string + name: + type: string + provider: + type: string + owner_type: + type: + - string + - 'null' + enum: + - organization + - user + - null + owner_id: + type: + - string + - 'null' + expires_at: + type: + - string + - 'null' + format: date-time + revoked: + type: boolean + description: 'Whether the secret has been revoked.' + Organization: + type: object + required: + - id + - name + - slug + - type + - status + properties: + id: + type: string + name: + type: string + slug: + type: string + type: + type: string + enum: + - customer + - reseller + status: + type: string + enum: + - active + - suspended + - deleted + parent_id: + type: + - string + - 'null' + created_at: + type: + - string + - 'null' + format: date-time + metadata: + type: + - object + - 'null' + additionalProperties: + type: string + description: 'Free-form text values kept on the organization; null when there are none.' + OrganizationDomain: + type: object + required: + - id + - organization_id + - domain + - verified + - capture + - record_name + - record_value + properties: + id: + type: string + organization_id: + type: string + domain: + type: string + description: 'An email domain the organization claims, like acme.com.' + verified: + type: boolean + verified_at: + type: + - string + - 'null' + format: date-time + capture: + type: boolean + description: "Whether everyone signing in with an address here is routed to the organization's SSO." + record_name: + type: string + description: 'Where to publish the DNS TXT record that proves the claim.' + record_value: + type: string + description: 'What to publish there.' + AssignableMemberRole: + type: string + enum: + - admin + - member + default: member + description: | + The built-in tiers this deployment hands out. `owner` is never assigned — ownership + moves with `transfer-ownership`. + Member: + type: object + required: + - id + - user_id + - organization_id + - role + - status + properties: + id: + type: string + description: "The membership's own id. Address a member by `user_id`." + user_id: + type: string + organization_id: + type: string + role: + type: string + enum: + - owner + - admin + - developer + - member + - viewer + status: + type: string + enum: + - active + - invited + - suspended + email: + type: + - string + - 'null' + name: + type: + - string + - 'null' + joined_at: + type: + - string + - 'null' + format: date-time + Invitation: + type: object + required: + - id + - organization_id + - email + - role + - roles + - status + properties: + id: + type: string + organization_id: + type: string + email: + type: string + format: email + role: + type: string + enum: + - owner + - admin + - developer + - member + - viewer + roles: + type: array + description: 'Ids of the access roles granted when the invitation is accepted.' + items: + type: string + status: + type: string + enum: + - pending + - accepted + - revoked + client_id: + type: + - string + - 'null' + description: 'The app it came from.' + return_to: + type: + - string + - 'null' + description: 'Where the person is sent after accepting.' + invited_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: + - string + - 'null' + format: date-time + Role: + type: object + required: + - id + - name + - tenant_assignable + - permissions + properties: + id: + type: string + key: + type: + - string + - 'null' + description: "The app's manifest key; null for a role made in the console." + name: + type: string + description: + type: + - string + - 'null' + client_id: + type: + - string + - 'null' + description: 'The app that declared it; null for an app-agnostic role.' + organization_id: + type: + - string + - 'null' + description: 'Set only for a role one organization made for itself.' + tenant_assignable: + type: boolean + description: | + `false` marks a STAFF role: an organization's own administrators can never grant + it, nor can an invitation carry it. This API can — see the role endpoints. + permissions: + type: array + items: + type: string + RoleAssignment: + type: object + required: + - role_id + - name + - user_id + - source + properties: + role_id: + type: string + key: + type: + - string + - 'null' + name: + type: string + client_id: + type: + - string + - 'null' + tenant_assignable: + type: boolean + user_id: + type: string + organization_id: + type: + - string + - 'null' + description: 'The organization it is held in; null for an environment-wide (staff) grant.' + source: + type: string + enum: + - manual + - pushed + - system + App: + type: object + required: + - id + - client_id + - name + - type + - client_type + - grant_types + - redirect_uris + - scopes + - first_party + properties: + id: + type: string + client_id: + type: string + name: + type: string + type: + type: string + enum: + - web + - spa + - cli + - service + - agent + - advanced + description: 'The kind, read back from its grants; `advanced` when they match no preset.' + client_type: + type: string + enum: + - confidential + - public + organization_id: + type: + - string + - 'null' + first_party: + type: boolean + grant_types: + type: array + items: + type: string + redirect_uris: + type: array + items: + type: string + post_logout_redirect_uris: + type: array + items: + type: string + scopes: + type: array + items: + type: string + manifest_url: + type: + - string + - 'null' + description: 'Where it publishes its roles-and-permissions manifest.' + access_token_ttl: + type: + - integer + - 'null' + description: "Its access-token lifetime in seconds; null for the install's default." + backchannel_logout_uri: + type: + - string + - 'null' + description: 'Where it is sent a logout token (OIDC Back-Channel Logout); null when it is not told.' + backchannel_logout_session_required: + type: boolean + api_key_prefix: + type: + - string + - 'null' + description: 'The prefix of its customer API keys; null when it accepts none.' + created_at: + type: + - string + - 'null' + format: date-time + client_secret: + type: + - string + - 'null' + description: 'Only on the response that created (or copied) the app, and only for an app that authenticates with a secret. Never again — null on an idempotent replay.' + AppSecret: + type: object + required: + - id + properties: + id: + type: string + hint: + type: + - string + - 'null' + description: 'The characters the secret ends in; null for one older than per-secret storage.' + created_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: + - string + - 'null' + format: date-time + description: 'Set on a secret a rotation replaced — it stops working then.' + last_used_at: + type: + - string + - 'null' + format: date-time + client_secret: + type: + - string + - 'null' + description: 'Only on the rotation that minted it. Never again — null on an idempotent replay.' + previous_expire_at: + type: + - string + - 'null' + format: date-time + description: 'On a rotation — when the secrets it replaced stop working.' + AppBlueprint: + type: object + description: | + An app's configuration without its identity or credentials — deterministic (lists + sorted), versioned, and refused rather than half-read when it carries a key this + server does not know. + required: + - kind + - version + - name + - client_type + additionalProperties: false + properties: + kind: + type: string + const: cbox-id.client-blueprint + version: + type: integer + const: 1 + name: + type: string + client_type: + type: string + enum: + - confidential + - public + token_endpoint_auth_method: + type: + - string + - 'null' + grant_types: + type: array + items: + type: string + redirect_uris: + type: array + items: + type: string + post_logout_redirect_uris: + type: array + items: + type: string + scopes: + type: array + items: + type: string + first_party: + type: boolean + manifest_url: + type: + - string + - 'null' + access_token_ttl: + type: + - integer + - 'null' + backchannel_logout_uri: + type: + - string + - 'null' + backchannel_logout_session_required: + type: boolean + api_key_prefix: + type: + - string + - 'null' + ApiScope: + type: object + required: + - key + properties: + key: + type: string + maxLength: 128 + description: + type: + - string + - 'null' + maxLength: 255 + tenant_requestable: + type: boolean + default: true + description: "Whether apps an organization owns may be granted it; false keeps it for the environment's own apps." + Api: + type: object + required: + - id + - identifier + - name + - scopes + properties: + id: + type: string + identifier: + type: string + description: "Absolute URI; the token's `aud`." + name: + type: string + organization_id: + type: + - string + - 'null' + description: 'null when the environment owns it.' + client_id: + type: + - string + - 'null' + description: 'The app whose roles and permissions it enforces.' + scopes: + type: array + items: + $ref: '#/components/schemas/ApiScope' + created_at: + type: + - string + - 'null' + format: date-time + Webhook: + type: object + required: + - id + - url + - organization_id + - event_types + - active + properties: + id: + type: string + url: + type: string + organization_id: + type: + - string + - 'null' + description: "null when the environment owns it: it receives EVERY organization's events." + event_types: + type: array + items: + type: string + active: + type: boolean + description: 'false while paused.' + consecutive_failures: + type: integer + last_success_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + secret: + type: + - string + - 'null' + description: | + The signing secret, on the create and rotate answers only — shown once, never + retrievable again. `null` on an idempotent replay of that answer. + InlineHook: + type: object + required: + - id + - url + - hook_point + - organization_id + - active + properties: + id: + type: string + url: + type: string + hook_point: + type: string + enum: + - token_minting + - post_login + - pre_registration + - post_registration + - pre_password_change + - post_password_change + organization_id: + type: + - string + - 'null' + description: 'null when the environment owns it: it runs for EVERY organization.' + active: + type: boolean + created_at: + type: + - string + - 'null' + format: date-time + secret: + type: + - string + - 'null' + description: 'The signing secret, on the create answer only — shown once. `null` on an idempotent replay.' + LogStream: + type: object + required: + - id + - name + - destination + - endpoint_url + - auth + - organization_id + - enabled + properties: + id: + type: string + name: + type: string + destination: + type: string + enum: + - splunk_hec + - elastic_ecs + - graylog_gelf + - cef_http + - generic_json + endpoint_url: + type: string + auth: + type: string + enum: + - none + - bearer + - splunk + - hmac + organization_id: + type: + - string + - 'null' + description: "null when the environment owns it: it carries EVERY organization's entries." + enabled: + type: boolean + consecutive_failures: + type: integer + last_success_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + secret: + type: + - string + - 'null' + description: 'A generated HMAC key, on the create answer only — shown once. `null` on an idempotent replay.' + DomainEvent: + type: object + required: + - id + - type + - organization_id + - occurred_at + - payload + properties: + id: + type: string + description: 'A ULID; pass the last one you saw as `after`.' + type: + type: string + description: 'e.g. `user.created` — the webhook event name.' + organization_id: + type: + - string + - 'null' + occurred_at: + type: + - string + - 'null' + format: date-time + payload: + type: object + AuditEntry: + type: object + required: + - id + - action + - actor_type + - actor_id + - organization_id + - target_type + - target_id + - context + - recorded_at + properties: + id: + type: string + description: 'A ULID; pass the last one you saw as `after`.' + action: + type: string + description: 'e.g. `webhook.created`.' + actor_type: + type: string + enum: + - user + - service + - system + - operator + - organization_member + actor_id: + type: + - string + - 'null' + description: "For `service`, the management key's or app's id." + organization_id: + type: + - string + - 'null' + target_type: + type: + - string + - 'null' + target_id: + type: + - string + - 'null' + context: + type: object + ip: + type: + - string + - 'null' + recorded_at: + type: + - string + - 'null' + format: date-time + ApiKey: + type: object + required: + - id + - prefix + - organization_id + - user_id + - client_id + - permissions + - status + - revoked + properties: + id: + type: string + name: + type: + - string + - 'null' + prefix: + type: string + description: 'What a person recognises the key by. Never the key.' + organization_id: + type: string + user_id: + type: string + description: 'Its holder.' + client_id: + type: string + description: 'The app whose API it is for.' + permissions: + type: array + items: + type: string + status: + type: string + enum: + - active + - expired + - revoked + revoked: + type: boolean + created_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: + - string + - 'null' + format: date-time + last_used_at: + type: + - string + - 'null' + format: date-time + revoked_at: + type: + - string + - 'null' + format: date-time + SupportSession: + type: object + required: + - id + - user_id + - organization_id + - client_id + - actor_id + - act + - reason + - scopes + - expires_at + properties: + id: + type: string + user_id: + type: string + description: "The customer's user being acted as." + organization_id: + type: string + client_id: + type: string + actor_id: + type: string + act: + type: object + required: + - sub + description: 'The RFC 8693 actor claim every token of the session carries.' + properties: + sub: + type: string + reason: + type: string + scopes: + type: array + items: + type: string + expires_at: + type: string + format: date-time + code: + type: + - string + - 'null' + description: 'The first authorization code, when `redirect_uri` and `code_challenge` were sent. Shown once.' + redirect_uri: + type: + - string + - 'null' + ApiKeyVerificationRequest: + type: object + required: + - key + properties: + key: + type: string + description: 'The API key your API was presented with.' + client_id: + type: string + description: 'For `client_secret_post`.' + client_secret: + type: string + description: 'For `client_secret_post`.' + client_assertion_type: + type: string + description: 'For `private_key_jwt`.' + client_assertion: + type: string + description: 'For `private_key_jwt`.' + ApiKeyVerification: + type: object + required: + - active + properties: + active: + type: boolean + key_id: + type: string + sub: + type: string + description: "The key's holder." + org: + type: string + org_role: + type: + - string + - 'null' + enum: + - owner + - admin + - developer + - member + - viewer + - null + permissions: + type: array + items: + type: string + client_id: + type: string + expires_at: + type: + - string + - 'null' + format: date-time + OAuthClientError: + type: object + required: + - error + description: "RFC 6749 §5.2 — the OAuth error shape, not this API's `{error, message}` envelope." + properties: + error: + type: string + enum: + - invalid_client + error_description: + type: string + User: + type: object + properties: + id: + type: string + email: + type: string + format: email + name: + type: + - string + - 'null' + status: + type: string + enum: + - active + - disabled + - locked + email_verified_at: + type: + - string + - 'null' + format: date-time + UserSession: + type: object + required: + - id + - expires_at + - impersonation + properties: + id: + type: string + user_agent: + type: + - string + - 'null' + ip: + type: + - string + - 'null' + last_active_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: string + format: date-time + impersonation: + type: boolean + description: 'Somebody else opened it as this person.' + Permission: + type: object + required: + - id + - name + - manual + - tenant_assignable + - orphaned + properties: + id: + type: string + name: + type: string + description: 'The `feature:action` key an app reads out of a token.' + description: + type: + - string + - 'null' + client_id: + type: + - string + - 'null' + description: 'The app that declared it; null for one authored here.' + organization_id: + type: + - string + - 'null' + description: "Set for one organization's own; null for the shared tier." + tenant_assignable: + type: boolean + description: 'Whether organizations may compose it into their own roles.' + manual: + type: boolean + description: 'Authored here rather than declared by an app.' + orphaned: + type: boolean + description: 'Its app stopped declaring it.' + SignInRules: + type: object + required: + - min_length + - require_breach_check + - max_age_days + - reuse_history + - mfa + - sso + - lockout_threshold + properties: + min_length: + type: integer + description: 'Minimum password length.' + require_breach_check: + type: boolean + description: 'Passwords found in known breaches are refused.' + max_age_days: + type: + - integer + - 'null' + description: 'Days before a password must be changed; null for no limit.' + reuse_history: + type: integer + description: 'Previous passwords that may not be reused.' + mfa: + type: string + enum: + - 'off' + - optional + - required + sso: + type: string + enum: + - 'off' + - preferred + - required + description: '`required` refuses every other way in.' + lockout_threshold: + type: + - integer + - 'null' + description: 'Failed attempts before a lockout; null for none.' + SignInPolicy: + type: object + required: + - organization_id + - policy + - baseline + - override + - inheriting + properties: + organization_id: + type: + - string + - 'null' + description: 'null for the environment baseline.' + policy: + $ref: '#/components/schemas/SignInRules' + description: "What is in force at this level: the baseline, or the organization's override tightened over it." + baseline: + $ref: '#/components/schemas/SignInRules' + override: + description: 'What the organization stored itself; null when it inherits (and always for the baseline).' + oneOf: + - $ref: '#/components/schemas/SignInRules' + - type: 'null' + inheriting: + type: boolean + description: 'True for an organization with no override of its own.' + SocialProvider: + type: object + required: + - id + - organization_id + - provider + - name + - protocol + - status + - callback_uri + description: 'Its client secret and private key are write-only and never returned.' + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + provider: + type: + - string + - 'null' + description: 'The catalogue key: google, github, apple…' + name: + type: string + protocol: + type: string + enum: + - oidc + - oauth2 + status: + type: string + callback_uri: + type: string + description: 'The redirect URI to register with the provider.' + created_at: + type: + - string + - 'null' + format: date-time + FrontendKey: + type: object + required: + - id + - name + - key + - mode + - origins + - active + description: 'A publishable key. Returned in full: it is public by design; the origins are the control.' + properties: + id: + type: string + name: + type: string + key: + type: string + mode: + type: string + enum: + - test + - live + origins: + type: array + items: + type: string + active: + type: boolean + created_at: + type: + - string + - 'null' + format: date-time + last_used_at: + type: + - string + - 'null' + format: date-time + revoked_at: + type: + - string + - 'null' + format: date-time + SamlApp: + type: object + required: + - id + - entity_id + - acs_url + - name_id_format + - name_id_attribute + - attribute_mappings + - want_authn_requests_signed + - has_certificate + - organization_id + - status + description: "An application that trusts this environment as its SAML identity provider. Its certificate is write-only. An application owned by an organization is only ever asserted to that organization's active members; one with no organization is open to every person in the environment." + properties: + id: + type: string + entity_id: + type: string + acs_url: + type: string + name_id_format: + type: string + name_id_attribute: + type: string + attribute_mappings: + type: array + items: + type: object + required: + - attribute + - field + properties: + attribute: + type: string + field: + type: string + want_authn_requests_signed: + type: boolean + has_certificate: + type: boolean + organization_id: + type: + - string + - 'null' + description: 'The organization whose active members alone may sign in to it; null for an environment-wide application.' + status: + type: string + created_at: + type: + - string + - 'null' + format: date-time + updated_at: + type: + - string + - 'null' + format: date-time + ErasureReceipt: + type: object + required: + - subject_id + - erased_at + - subject_pseudonymised + - audit_entry_id + - steps + description: 'The record of one erasure (GDPR Art. 17): what each store did, in numbers. Carries no personal data — safe to log, file with your Art. 30 records, or hand to the person who asked.' + properties: + subject_id: + type: string + description: "The erased person's id. Kept: the row is pseudonymised in place, not deleted." + erased_at: + type: string + format: date-time + subject_pseudonymised: + type: boolean + description: "Whether the account row's email and name were replaced with placeholders." + audit_entry_id: + type: + - string + - 'null' + description: 'The `user.erased` tombstone on the audit trail.' + steps: + type: array + description: 'One entry per store, in the order they ran — including stores that held nothing, so a receipt shows a store was checked.' + items: + type: object + required: + - step + - counts + - note + properties: + step: + type: string + description: 'The store, e.g. `identity.credentials`, `oauth.grants`, `devices.devices`.' + counts: + type: object + description: 'What was removed or rewritten there, by item.' + additionalProperties: + type: integer + note: + type: + - string + - 'null' + description: 'Why a step could not act, when it could not.' + LegacyLogin: + type: object + required: + - declared + - url + - client_id + - declared_by + - approved + - approved_at + - approved_by + description: 'The legacy sign-in endpoint an app declared for migration. Its shared secret is never returned.' + properties: + declared: + type: boolean + url: + type: + - string + - 'null' + client_id: + type: + - string + - 'null' + description: 'The app that declared it.' + declared_by: + type: + - string + - 'null' + description: "That app's name." + approved: + type: boolean + approved_at: + type: + - string + - 'null' + format: date-time + approved_by: + type: + - string + - 'null' + description: 'The person or key that approved it.' + ThemeMode: + type: object + properties: + primary: + type: string + background: + type: string + foreground: + type: string + muted: + type: string + Appearance: + type: object + required: + - organization_id + - customized + - theme + - logo + properties: + organization_id: + type: + - string + - 'null' + description: 'null for the environment default.' + customized: + type: boolean + description: 'Whether this level has a theme of its own.' + theme: + type: object + properties: + preset: + type: string + radius: + type: string + font: + type: string + light: + $ref: '#/components/schemas/ThemeMode' + dark: + $ref: '#/components/schemas/ThemeMode' + logo: + type: + - string + - 'null' + WhitelabelBranding: + type: object + required: + - organization_id + - palette + - app_name + - email_from_name + - email_template + - logo_url + - favicon_url + properties: + organization_id: + type: + - string + - 'null' + description: 'null for the environment default.' + palette: + type: object + additionalProperties: + type: + - string + - 'null' + app_name: + type: + - string + - 'null' + email_from_name: + type: + - string + - 'null' + email_template: + type: + - string + - 'null' + logo_url: + type: + - string + - 'null' + description: 'Uploaded from the console.' + favicon_url: + type: + - string + - 'null' + description: 'Uploaded from the console.' + CustomDomain: + type: object + required: + - domain + - verified_at + - pending + properties: + domain: + type: + - string + - 'null' + description: 'The verified domain serving this environment.' + verified_at: + type: + - string + - 'null' + format: date-time + pending: + description: 'A domain waiting for its DNS proof, and the TXT record to publish.' + oneOf: + - + type: object + required: + - domain + - record_name + - record_value + properties: + domain: + type: string + record_name: + type: string + record_value: + type: string + - type: 'null' + SsoConnection: + type: object + required: + - id + - organization_id + - name + - type + - status + - active + - config + description: 'A SAML or OIDC connection an organization signs in through. Its certificate, client secret and signing key are write-only.' + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + description: 'null when the environment owns it: it signs people in and enrols them nowhere.' + name: + type: string + type: + type: string + enum: + - saml + - oidc + - oauth2 + provider: + type: + - string + - 'null' + description: "The catalogue provider a social sign-in connection came from; null for an organization's own identity provider." + status: + type: string + enum: + - draft + - active + - inactive + active: + type: boolean + complete: + type: boolean + description: 'false for a draft created with pending_idp whose identity-provider details are still missing. Activation refuses it.' + config: + type: object + description: 'The settings that are not secrets.' + properties: + idp_entity_id: + type: string + idp_sso_url: + type: string + sp_entity_id: + type: string + sp_acs_url: + type: string + issuer: + type: string + client_id: + type: string + service_provider: + type: + - object + - 'null' + description: "What to paste into the identity provider: this connection's own entity id and ACS URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection." + properties: + sp_entity_id: + type: string + sp_acs_url: + type: string + format: uri + redirect_uri: + type: string + format: uri + created_at: + type: + - string + - 'null' + format: date-time + updated_at: + type: + - string + - 'null' + format: date-time + SsoCertificates: + type: object + required: + - connection_id + - organization_id + - expires_at + - days_remaining + - certificates + description: "A SAML connection's signing certificates. The certificates themselves are never returned." + properties: + connection_id: + type: string + organization_id: + type: + - string + - 'null' + expires_at: + type: + - string + - 'null' + format: date-time + description: 'When the connection stops working if nothing is done: the latest expiry among the certificates it trusts.' + days_remaining: + type: + - integer + - 'null' + certificates: + type: array + items: + type: object + required: + - role + - readable + - fingerprint_sha256 + - subject + - issuer + - not_before + - not_after + - days_remaining + - expired + properties: + role: + type: string + enum: + - primary + - staged + description: 'primary signs today; staged is trusted beside it until activated.' + readable: + type: boolean + description: 'false for a certificate on file that could not be parsed — still trusted.' + fingerprint_sha256: + type: + - string + - 'null' + subject: + type: + - string + - 'null' + issuer: + type: + - string + - 'null' + not_before: + type: + - string + - 'null' + format: date-time + not_after: + type: + - string + - 'null' + format: date-time + days_remaining: + type: + - integer + - 'null' + expired: + type: + - boolean + - 'null' + checks: + type: array + description: 'On the stage answer only: what the new certificate was tested for.' + items: + type: object + required: + - check + - passed + properties: + check: + type: string + enum: + - readable + - not_expired + - key_strength + - not_on_file + - valid_now + passed: + type: boolean + LogStreamTest: + type: object + required: + - id + - delivered + - error + - tested_at + properties: + id: + type: string + delivered: + type: boolean + description: 'Whether the destination accepted the test entry.' + error: + type: + - string + - 'null' + description: "The destination's refusal, scrubbed of the stream's secret." + tested_at: + type: string + format: date-time + SsoDomain: + type: object + required: + - id + - organization_id + - domain + - verified + - capture + - verification + properties: + id: + type: string + organization_id: + type: string + domain: + type: string + verified: + type: boolean + verified_at: + type: + - string + - 'null' + format: date-time + capture: + type: boolean + description: "Everyone with an address at the domain must sign in through the organization's SSO." + verification: + description: 'The DNS TXT record that proves the domain, while it is unverified.' + oneOf: + - + type: object + required: + - type + - name + - value + properties: + type: + type: string + enum: + - TXT + name: + type: string + value: + type: string + - type: 'null' + SamlMetadata: + type: object + required: + - idp_entity_id + - idp_sso_url + - idp_x509cert + description: "An identity provider's settings read from its SAML metadata. Nothing is stored." + properties: + idp_entity_id: + type: string + idp_sso_url: + type: string + idp_x509cert: + type: string + description: "The IdP's public signing certificate, from its own metadata." + PortalLink: + type: object + required: + - id + - organization_id + - intents + - url + - expires_at + - emailed_to + properties: + id: + type: string + organization_id: + type: string + intents: + type: array + description: 'What the link may set up.' + items: + type: string + enum: + - sso + - dsync + - domain_verification + - log_streams + - certificate_renewal + - audit_logs + emailed_to: + type: + - string + - 'null' + description: 'The address the link was mailed to, or null when it was not sent.' + url: + type: + - string + - 'null' + description: | + The one-time setup link — the whole credential. Shown once; `null` on an + idempotent replay of this answer. + expires_at: + type: + - string + - 'null' + format: date-time + AuditLogEvent: + type: object + description: | + One audit event your app sent. `hash` covers `id`, `organization_id`, `sequence`, + `action`, `occurred_at`, `actor`, `targets`, `context` and `metadata`, chained to + the organization's previous event by `prev_hash` — see the Audit Logs guide for how + to verify it yourself. + required: + - id + - organization_id + - sequence + - action + - occurred_at + - actor + - targets + - context + - metadata + - schema_version + - received_at + - prev_hash + - hash + properties: + id: + type: string + organization_id: + type: string + sequence: + type: integer + description: "The event's place in its organization's chain, from 1." + action: + type: string + description: 'e.g. `invoice.voided`.' + occurred_at: + type: string + format: date-time + description: 'When it happened, as the sender said, to the millisecond.' + actor: + type: object + required: + - id + - type + - name + - metadata + properties: + id: + type: string + type: + type: string + name: + type: + - string + - 'null' + metadata: + type: + - object + - 'null' + targets: + type: array + items: + type: object + required: + - id + - type + - name + - metadata + properties: + id: + type: string + type: + type: string + name: + type: + - string + - 'null' + metadata: + type: + - object + - 'null' + context: + type: object + required: + - location + - user_agent + properties: + location: + type: + - string + - 'null' + user_agent: + type: + - string + - 'null' + metadata: + type: + - object + - 'null' + schema_version: + type: + - integer + - 'null' + description: "The version of its action's schema it was checked against; null when the action has none." + received_at: + type: + - string + - 'null' + format: date-time + description: 'When it arrived. Retention counts from here.' + prev_hash: + type: string + description: "The previous event's hash; 64 zeros for an organization's first." + hash: + type: string + AuditLogEventBatch: + type: object + required: + - events + properties: + events: + type: array + description: 'The recorded events, in the order they were sent.' + items: + $ref: '#/components/schemas/AuditLogEvent' + AuditLogSchema: + type: object + required: + - id + - action + - version + - targets + - actor_metadata + - metadata + - created_at + - updated_at + properties: + id: + type: string + action: + type: string + version: + type: integer + description: 'Starts at 1; every replacement is the next.' + targets: + type: + - array + - 'null' + description: "The target types this action's events may name; null allows any." + items: + type: object + required: + - type + - metadata + properties: + type: + type: string + metadata: + type: + - object + - 'null' + actor_metadata: + type: + - object + - 'null' + description: 'A metadata schema (a subset of JSON Schema).' + metadata: + type: + - object + - 'null' + description: 'A metadata schema (a subset of JSON Schema).' + created_at: + type: + - string + - 'null' + format: date-time + updated_at: + type: + - string + - 'null' + format: date-time + AuditLogExport: + type: object + required: + - id + - organization_id + - state + - filters + - row_count + - url + - created_at + - completed_at + - expires_at + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + description: 'null for an export across the whole environment.' + state: + type: string + enum: + - pending + - ready + - failed + - expired + filters: + type: object + description: 'The filters it was asked with.' + row_count: + type: + - integer + - 'null' + url: + type: + - string + - 'null' + description: | + Where the CSV downloads from once `ready` — signed, and valid for a few minutes; + read the export again for a fresh one. null otherwise, and on an idempotent replay. + created_at: + type: + - string + - 'null' + format: date-time + completed_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: + - string + - 'null' + format: date-time + description: 'When the file is deleted.' + AuditLogSettings: + type: object + required: + - retention_days + - strict_schemas + properties: + retention_days: + type: integer + description: 'Events are kept this many days after they arrive.' + strict_schemas: + type: boolean + description: 'Whether an action with no schema is refused.' + AuditLogVerification: + type: object + required: + - organization_id + - valid + - complete + - verified_count + - first_sequence + - last_sequence + - head_sequence + - broken_at_sequence + - reason + properties: + organization_id: + type: string + valid: + type: boolean + description: 'Every event checked is unchanged and in place.' + complete: + type: boolean + description: "The check reached the chain's head. When false, continue from last_sequence + 1." + verified_count: + type: integer + first_sequence: + type: + - integer + - 'null' + last_sequence: + type: + - integer + - 'null' + head_sequence: + type: integer + broken_at_sequence: + type: + - integer + - 'null' + reason: + type: + - string + - 'null' + enum: + - missing + - link + - hash + - truncated + - null + Directory: + type: object + required: + - id + - organization_id + - name + - provider + - pull + - active + - status + description: "A directory syncing an organization's people in. Its token and credentials are never returned after they are minted." + properties: + id: + type: string + organization_id: + type: string + name: + type: string + provider: + type: string + enum: + - scim + - google_workspace + - microsoft_entra + pull: + type: boolean + description: 'true when this platform fetches from the provider; false for a SCIM endpoint the provider posts to.' + active: + type: boolean + status: + type: string + enum: + - active + - paused + scim_base_url: + type: + - string + - 'null' + description: 'Where the identity provider sends SCIM requests. null for a pull directory.' + last_synced_at: + type: + - string + - 'null' + format: date-time + last_sync_error: + type: + - string + - 'null' + created_at: + type: + - string + - 'null' + format: date-time + bearer_token: + type: + - string + - 'null' + description: | + The SCIM bearer token, on the create and rotate answers only — shown once, never + retrievable again. `null` on an idempotent replay of that answer. + DirectoryGroup: + type: object + required: + - id + - directory_id + - name + - role_ids + properties: + id: + type: string + directory_id: + type: string + name: + type: string + external_id: + type: + - string + - 'null' + role_ids: + type: array + items: + type: string + description: 'The roles everyone in the group holds.' + ProvisioningTarget: + type: object + required: + - id + - organization_id + - name + - base_url + - auth_scheme + - active + description: 'A downstream app people are pushed to over SCIM. Its credential is write-only.' + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + description: "null when it receives every organization's people." + name: + type: string + base_url: + type: string + auth_scheme: + type: string + enum: + - bearer + - oauth2_client_credentials + token_url: + type: + - string + - 'null' + client_id: + type: + - string + - 'null' + active: + type: boolean + description: 'false while paused.' + consecutive_failures: + type: integer + last_success_at: + type: + - string + - 'null' + format: date-time + last_error: + type: + - string + - 'null' + created_at: + type: + - string + - 'null' + format: date-time + SodPolicy: + type: object + required: + - id + - organization_id + - name + - role_ids + - active + description: 'A role-conflict (segregation of duties) rule: roles no one person may hold together.' + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + description: 'null when it binds every organization.' + name: + type: string + description: + type: + - string + - 'null' + role_ids: + type: array + items: + type: string + active: + type: boolean + created_at: + type: + - string + - 'null' + format: date-time + AccessReview: + type: object + required: + - id + - organization_id + - staff + - name + - status + - open + - pending_policy + - item_count + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + description: 'null for a staff review of environment-wide grants.' + staff: + type: boolean + name: + type: string + status: + type: string + enum: + - open + - closed + open: + type: boolean + pending_policy: + type: string + enum: + - revoke + - certify + description: 'What closing does to an item nobody decided.' + item_count: + type: integer + due_at: + type: + - string + - 'null' + format: date-time + closed_at: + type: + - string + - 'null' + format: date-time + created_by: + type: + - string + - 'null' + description: 'The person or key that opened it.' + created_at: + type: + - string + - 'null' + format: date-time + AccessReviewItem: + type: object + required: + - id + - review_id + - subject_id + - access_type + - access_ref + - decision + - applied + properties: + id: + type: string + review_id: + type: string + subject_id: + type: string + access_type: + type: string + enum: + - role + - membership + - environment_role + access_ref: + type: string + description: 'The role id, or the organization id of a membership.' + organization_id: + type: + - string + - 'null' + decision: + type: string + enum: + - pending + - certified + - revoked + decided_by: + type: + - string + - 'null' + decided_at: + type: + - string + - 'null' + format: date-time + note: + type: + - string + - 'null' + applied: + type: boolean + description: 'Whether a revoke was carried out when the review closed.' + application_note: + type: + - string + - 'null' + description: 'Why a revoke could not be applied.' + TokenVaultSecret: + type: object + required: + - id + - organization_id + - name + - provider + - status + - revoked + description: 'A downstream credential in the token vault. Its value is write-only.' + properties: + id: + type: string + organization_id: + type: + - string + - 'null' + description: "null for the environment's own secrets." + name: + type: string + provider: + type: string + status: + type: string + enum: + - active + - expired + - revoked + revoked: + type: boolean + expires_at: + type: + - string + - 'null' + format: date-time + rotated_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + grants: + type: array + items: + type: string + description: 'The OAuth client ids that may lease it. Not on lists.' + AgentRequest: + type: object + description: "A pending request from an agent to act as one of this environment's people (OIDC CIBA)." + required: + - id + - client_id + - app + - subject + - scopes + - expires_at + properties: + id: + type: string + client_id: + type: string + app: + type: string + description: "The app's name, or its client id when it has none." + subject: + type: object + description: 'Whom the agent asks to act as.' + required: + - id + - email + properties: + id: + type: string + email: + type: + - string + - 'null' + description: 'Null when that person no longer exists — the request can still be denied.' + binding_message: + type: + - string + - 'null' + description: 'The message the person sees on their device, to recognise the request.' + scopes: + type: array + items: + type: string + expires_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + PageMeta: + type: object + properties: + limit: + type: integer + has_more: + type: boolean + next_cursor: + type: + - string + - 'null' + Error: + type: object + required: + - error + - message + properties: + error: + type: string + description: 'Stable machine code' + message: + type: string + description: 'Human-readable explanation' + request_id: + type: string + description: 'The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem.' + errors: + type: object + description: | + Present on `validation_failed` only: the offending request fields, each + mapped to its messages. + additionalProperties: + type: array + items: + type: string + BearerChallenge: + type: object + required: + - error + - error_description + description: | + RFC 6750 §3 error shape, used by the OAuth-token-authenticated Token Vault + endpoints. NOT the `{error, message}` envelope — restating the envelope here + would be a protocol conformance bug. + properties: + error: + type: string + enum: + - invalid_request + - invalid_token + - insufficient_scope + error_description: + type: string + responses: + ApprovalRequired: + description: | + Held for a person's approval: this key's policy requires its owner to approve this + action on their device first. Poll `approval.poll_url`, then repeat the same request + (same body, same `Idempotency-Key`) with `Cbox-Approval: `. + content: + application/json: + schema: + type: object + required: + - error + - message + - approval + properties: + error: + type: string + enum: + - approval_required + message: + type: string + approval: + type: object + required: + - id + - status + - binding_code + - expires_at + - poll_url + properties: + id: + type: string + status: + type: string + binding_code: + type: string + expires_at: + type: string + format: date-time + poll_url: + type: string + format: uri + Unauthorized: + description: 'Missing or invalid environment API key.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Forbidden: + description: 'The key is missing the scope this endpoint requires.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: 'No such resource in this environment.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + EnvironmentRequired: + description: | + A `WorkspaceAccessToken` on the platform root's host named no environment + (`error: environment_required`): send `Cbox-Environment`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UnprocessableEntity: + description: | + Validation failed (`error: validation_failed`, with a field-keyed `errors` map), + or a uniqueness conflict (e.g. slug/email taken). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Conflict: + description: | + The request conflicts with the resource's state; `error` names which rule + (`last_owner`, `already_member`, `not_pending`, `role_conflict`, …). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + MailFailed: + description: 'The mail server refused the invitation mail (`error: mail_failed`). Retry.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + TooManyRequests: + description: | + The key's rate limit for this plane is exhausted. The bucket is keyed on the API + KEY, so this reflects only your own traffic. + headers: + Retry-After: + description: 'Seconds to wait before retrying.' + schema: + type: integer + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + BearerUnauthorized: + description: 'Missing, expired, or wrongly-audienced access token (RFC 6750 challenge).' + headers: + WWW-Authenticate: + description: 'Bearer error="invalid_token", error_description="…"' + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/BearerChallenge' + ApprovalNotActionable: + description: | + The request is no longer pending. Deliberately uniform — unknown, expired, already + answered and belonging-to-another-subject are ONE response with a constant + message, so someone holding a stolen handset cannot learn whether a given request + id exists or whose it is. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: approval_not_actionable + message: 'This request is no longer pending. It may have expired or already been answered.' + BearerForbidden: + description: 'The access token does not carry the scope this endpoint requires.' + headers: + WWW-Authenticate: + description: 'Bearer error="insufficient_scope", scope="…"' + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/BearerChallenge' diff --git a/openapi/platform.yaml b/openapi/platform.yaml new file mode 100644 index 0000000..487a78e --- /dev/null +++ b/openapi/platform.yaml @@ -0,0 +1,953 @@ +# GENERATED by `php artisan openapi:build` from resources/openapi/base/platform.yaml +# and the action registry (app/Actions). Edit the base or the action, never this file. +openapi: 3.1.0 +info: + title: 'Cbox ID — Operator API' + version: 1.0.0 + description: | + The **deployment itself**, for the staff who run it: stand up customer workspaces, + create and bootstrap environments, manage organizations inside any environment, and + keep the operator roster. Everything the console's Platform pages do, as the same + actions — the same rules, refusals and audit trail. + + **Who may call it.** Only a platform OPERATOR, through an access token they delegated + carrying the `operator:*` scope each operation names — the token the platform root + issues them for its `/mcp` (`cbox login` at the root, or an agent signed in there). **No management key is accepted + here** — not an environment key (`cbid_env_…`), not a workspace key (`cbid_ws_…`): + those speak for a customer, and this API is the deployment above every customer. A + token whose person is not an active operator is refused with `403`; anything else + without a valid operator token gets `401`. + + **Scopes.** `operator:workspaces:write`, `operator:environments:write`, + `operator:organizations:write`, `operator:operators:write` — split by blast radius, so + a token that only onboards customers cannot add an operator. + + **Every write is critical.** Each one changes who can reach the deployment or a + customer's people. Suspensions and reactivations take the state you want + (`{"status": "suspended"}`), so a retry never undoes itself. + + **Idempotency.** A write takes an `Idempotency-Key` header: a retry with the same key + and body gets the first answer back, marked `Idempotent-Replayed: true`, for 24 hours. + + **Envelope.** Every success returns `{ "data": … }`. Every error returns + `{ "error": "", "message": "" }` — validation failures add a + field-keyed `errors` map. +servers: + - url: 'https://api.cboxid.com/api/v1' + description: 'Hosted (platform-root host)' + - + url: 'https://{host}/api/v1' + description: 'Self-hosted (the platform-root host)' + variables: + host: + default: id.example.com +security: + - + OperatorToken: [] +tags: + - name: Workspaces + description: 'The customers on the deployment.' + - name: Environments + description: 'Every environment on the deployment, whoever owns it.' + - name: Organizations + description: 'Organizations inside any one environment.' + - name: Operators + description: 'The people who run the deployment.' + - name: Approvals + description: "Where a held operation's approval stands." +paths: + '/platform/action-approvals/{id}': + get: + tags: + - Approvals + summary: 'Where an approval you asked for stands' + description: | + The `poll_url` of a `202 approval_required`: an approval this token raised, and only those — another token's id is a 404. `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + parameters: + - + name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + required: + - id + - status + properties: + id: + type: string + status: + type: string + enum: + - pending + - approved + - denied + - expired + - consumed + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + /platform/environments: + post: + tags: + - Environments + summary: 'Create an environment on the deployment, with its own signing key. A domain is verified separately, by DNS.' + description: 'Requires scope `operator:environments:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_environments_create + x-action: platform.environments.create + x-scope: 'operator:environments:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 190 + domain: + type: + - string + - 'null' + description: 'The domain it will serve on, once verified by DNS. Checked to be free; not written yet.' + maxLength: 190 + required: + - name + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformEnvironment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/platform/environments/{environment_id}/organizations': + post: + tags: + - Organizations + summary: 'Create an organization inside an environment, optionally under a parent organization of the same environment.' + description: 'Requires scope `operator:organizations:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_organizations_create + x-action: platform.organizations.create + x-scope: 'operator:organizations:write' + x-danger: critical + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 190 + type: + type: string + enum: + - customer + - reseller + parent_id: + type: + - string + - 'null' + description: 'An organization of the same environment to create it under.' + required: + - name + - type + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformOrganization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/platform/environments/{environment_id}/organizations/{organization_id}/parent': + put: + tags: + - Organizations + summary: 'Move an organization under another organization of the same environment, or to the top of its hierarchy.' + description: 'Requires scope `operator:organizations:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_organizations_move + x-action: platform.organizations.move + x-scope: 'operator:organizations:write' + x-danger: critical + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - + name: organization_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + parent_id: + type: + - string + - 'null' + description: 'The new parent, of the same environment; null for the top of the hierarchy.' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformOrganization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/platform/environments/{environment_id}/organizations/{organization_id}/status': + put: + tags: + - Organizations + summary: 'Suspend an organization inside an environment (its members can no longer sign in to it) or reactivate it.' + description: 'Requires scope `operator:organizations:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_organizations_set_status + x-action: platform.organizations.set_status + x-scope: 'operator:organizations:write' + x-danger: critical + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - + name: organization_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - active + - suspended + required: + - status + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformOrganization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/platform/environments/{environment_id}/provision': + post: + tags: + - Environments + summary: 'Bootstrap an environment with its first organization and an owner administrator, so people can sign in to it.' + description: 'Requires scope `operator:environments:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_environments_provision + x-action: platform.environments.provision + x-scope: 'operator:environments:write' + x-danger: critical + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization_name: + type: string + maxLength: 190 + admin_name: + type: string + maxLength: 190 + admin_email: + type: string + format: email + maxLength: 190 + admin_password: + type: string + description: "Write-only. Must meet the environment's own password policy." + maxLength: 200 + required: + - organization_name + - admin_name + - admin_email + - admin_password + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ProvisionedEnvironment' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /platform/operators: + post: + tags: + - Operators + summary: 'Add a platform operator: a person with authority over the whole deployment.' + description: 'Requires scope `operator:operators:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_operators_create + x-action: platform.operators.create + x-scope: 'operator:operators:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 190 + email: + type: string + format: email + maxLength: 190 + password: + type: string + description: 'Write-only. At least 12 characters, and not one known from a breach.' + minLength: 12 + maxLength: 200 + required: + - name + - email + - password + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformOperator' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/platform/operators/{operator_id}/status': + put: + tags: + - Operators + summary: 'Suspend a platform operator, or reactivate one. Never yourself, and never the last active operator.' + description: 'Requires scope `operator:operators:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_operators_set_status + x-action: platform.operators.set_status + x-scope: 'operator:operators:write' + x-danger: critical + parameters: + - + name: operator_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - active + - suspended + required: + - status + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformOperator' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /platform/workspaces: + post: + tags: + - Workspaces + summary: 'Create a workspace with its owner, first project and first environment; the owner is emailed a link to set their password.' + description: 'Requires scope `operator:workspaces:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_workspaces_create + x-action: platform.workspaces.create + x-scope: 'operator:workspaces:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: "The workspace's name — usually the company it belongs to." + minLength: 2 + maxLength: 120 + owner_email: + type: string + description: 'The owner. An address that already has an account keeps it; the workspace is added to it.' + format: email + maxLength: 255 + owner_name: + type: string + minLength: 2 + maxLength: 120 + environment_limit: + type: integer + description: "The first project's environment allowance: 1, 2, 3, 5, 10 or 25." + minimum: 1 + maximum: 25 + required: + - name + - owner_email + - owner_name + - environment_limit + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ProvisionedWorkspace' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/platform/workspaces/{workspace_id}/status': + put: + tags: + - Workspaces + summary: 'Suspend a workspace (its people can no longer sign in, its environments stop serving) or reactivate it.' + description: 'Requires scope `operator:workspaces:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical.' + operationId: platform_workspaces_set_status + x-action: platform.workspaces.set_status + x-scope: 'operator:workspaces:write' + x-danger: critical + parameters: + - + name: workspace_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - active + - suspended + required: + - status + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/PlatformOrganization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' +components: + securitySchemes: + OperatorToken: + type: http + scheme: bearer + description: 'An access token delegated by an active platform operator, carrying `operator:*` scopes. Management keys are refused.' + parameters: + CboxApproval: + name: Cbox-Approval + in: header + required: false + description: 'The id of an approval this request was held for, once the person approved it. Spent by this request, once.' + schema: + type: string + IdempotencyKey: + name: Idempotency-Key + in: header + required: false + description: 'Any unique string (a UUID). A retry with the same key and request gets the first answer back.' + schema: + type: string + maxLength: 255 + responses: + ApprovalRequired: + description: "Held for a person's approval. Poll `approval.poll_url`, then repeat the same request with `Cbox-Approval`." + content: + application/json: + schema: + type: object + required: + - error + - message + - approval + properties: + error: + type: string + enum: + - approval_required + message: + type: string + approval: + type: object + required: + - id + - status + - binding_code + - expires_at + - poll_url + properties: + id: + type: string + status: + type: string + binding_code: + type: string + expires_at: + type: string + format: date-time + poll_url: + type: string + format: uri + Unauthorized: + description: 'No valid operator token. Management keys always get this.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Forbidden: + description: "The token's person is not an active platform operator, or the token lacks the required scope." + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: 'No such workspace, environment, organization or operator on this deployment.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Conflict: + description: 'A request with this `Idempotency-Key` is still being processed (`idempotency_in_progress`). Retry shortly.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UnprocessableEntity: + description: | + Validation failed (`error: validation_failed`, with a field-keyed `errors` map), or the + request was refused for a domain reason — an address already an operator, a password + the environment's policy rejects, the last active operator. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + TooManyRequests: + description: "The token's rate limit for this API is exhausted." + headers: + Retry-After: + description: 'Seconds to wait before retrying.' + schema: + type: integer + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + ProvisionedWorkspace: + type: object + required: + - id + - name + - owner + - project_id + - environment_id + - owner_invited + properties: + id: + type: string + name: + type: string + owner: + type: object + required: + - id + - email + properties: + id: + type: string + email: + type: string + format: email + project_id: + type: string + description: "The workspace's first project." + environment_id: + type: string + description: "That project's first environment." + owner_invited: + type: boolean + description: 'Whether the owner was emailed a link to set their password.' + PlatformOrganization: + type: object + required: + - id + - name + - slug + - type + - status + properties: + id: + type: string + name: + type: string + slug: + type: string + type: + type: string + enum: + - customer + - reseller + status: + type: string + enum: + - active + - suspended + - deleted + parent_id: + type: + - string + - 'null' + environment_id: + type: + - string + - 'null' + description: 'The environment it lives in.' + created_at: + type: + - string + - 'null' + format: date-time + PlatformEnvironment: + type: object + required: + - id + - name + - slug + - status + properties: + id: + type: string + name: + type: string + slug: + type: string + domain: + type: + - string + - 'null' + description: 'The verified domain it serves on — null until one is verified by DNS.' + status: + type: string + project_id: + type: + - string + - 'null' + description: 'The project that owns it; null for one no customer owns yet.' + created_at: + type: + - string + - 'null' + format: date-time + ProvisionedEnvironment: + type: object + required: + - environment_id + - organization + - admin + properties: + environment_id: + type: string + organization: + $ref: '#/components/schemas/PlatformOrganization' + admin: + type: object + required: + - id + - email + properties: + id: + type: string + email: + type: string + format: email + PlatformOperator: + type: object + required: + - id + - email + - status + properties: + id: + type: string + email: + type: string + format: email + name: + type: + - string + - 'null' + status: + type: string + enum: + - active + - suspended + created_at: + type: + - string + - 'null' + format: date-time + Error: + type: object + required: + - error + - message + properties: + error: + type: string + description: 'Stable machine code' + message: + type: string + description: 'Human-readable explanation' + request_id: + type: string + description: 'The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem.' + errors: + type: object + description: 'Present on `validation_failed` only — the offending fields, each mapped to its messages.' + additionalProperties: + type: array + items: + type: string diff --git a/openapi/workspace.yaml b/openapi/workspace.yaml new file mode 100644 index 0000000..130b2ff --- /dev/null +++ b/openapi/workspace.yaml @@ -0,0 +1,1852 @@ +# GENERATED by `php artisan openapi:build` from resources/openapi/base/workspace.yaml +# and the action registry (app/Actions). Edit the base or the action, never this file. +openapi: 3.1.0 +info: + title: 'Cbox ID — Workspace Management API' + version: 1.0.0 + description: | + The **global organization-management plane** — above every environment. Manage the + workspace, its **projects** (IdP products, each a billing anchor) and their + environments, its team, and its keys with a workspace key. + + **Scopes.** A workspace key carries a ROLE and, optionally, SCOPES. The role bounds + everything the key may do — the same capabilities a person holding that role has in + the console — and scopes narrow it further: `workspace:read`, `projects:write`, + `environments:write`, `team:read`, `team:write`, `keys:write`, `settings:write`. A key + minted with no scopes is bounded by its role alone. Each operation names the scope and + the capability it needs; a key missing either gets `403`. + + **As a member of the team.** Beside a key, the plane takes an access token the platform + root issued one of the workspace's people (`WorkspaceAccessToken` — `cbox login` at the + root, or an agent signed in there). It is bounded the same two ways: the person's + membership role must hold the capability, and the token must carry the scope. What it + does is recorded as the member, and every critical operation waits for the person's + approval on their device. With the same token, each environment of the workspace is + reached from the root's host through the environment API, naming it in + `Cbox-Environment`. + + **Bootstrapping an environment.** `POST /workspace/environments` with `initial_key` + creates the environment and mints its first management key (`cbid_env_…`) in one call. + That key is returned once, as `initial_key.token`, and works on the environment's own + host (its `issuer`) against the environment management API. + + **Idempotency.** A write takes an `Idempotency-Key` header: a retry with the same key + and body gets the first answer back, marked `Idempotent-Replayed: true`, for 24 hours — + with any secret it carried (a key's `token`) replaced by `null`. + + This is distinct from the per-environment API (OIDC, organizations, users, SCIM), + which is served on each environment's own host and authenticated with + environment-scoped credentials. Credentials never cross planes: an organization key + (`cbid_ws_…`) cannot call the environment API, and vice versa. + + **Envelope.** Every success returns `{ "data": … }` (an object, or an array with + a sibling `meta` on lists). Every error returns `{ "error": "", "message": + "" }` — including validation failures, which add a field-keyed + `errors` map alongside the pair. + + **Rate limits.** Buckets are keyed on the API KEY, not the caller's IP, so a CI + runner behind a shared NAT gets its own allowance. A `429` carries `Retry-After`. +servers: + - url: 'https://api.cboxid.com/api/v1' + description: 'Hosted (global organization plane)' + - + url: 'https://{host}/api/v1' + description: Self-hosted + variables: + host: + default: id.example.com +security: + - + OrganizationApiKey: [] + - + WorkspaceAccessToken: [] +tags: + - name: Organization + - name: Projects + - name: Environments + - name: Members + - name: Keys + description: "Workspace keys, and the management keys of the workspace's environments." + - name: Approvals + description: "Actions a key's policy holds for a person's approval." +paths: + /workspace: + get: + tags: + - Organization + summary: 'Get the organization' + operationId: workspace_get + x-action: workspace.get + description: "Requires scope `workspace:read` (any role). Returns the organization's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer)." + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Organization' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + patch: + tags: + - Organization + summary: 'Rename the workspace.' + description: 'Requires scope `settings:write` and a role that may `manage-members`. Danger: write.' + operationId: workspace_settings_update + x-action: workspace.settings.update + x-scope: 'settings:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + required: + - name + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/action-approvals/{id}': + get: + tags: + - Approvals + summary: 'Where an approval this key asked for stands' + description: | + Any workspace key — or a member's token — may poll the approvals it raised, and only + those: another credential's id is a 404. `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + parameters: + - + name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + required: + - id + - status + properties: + id: + type: string + status: + type: string + enum: + - pending + - approved + - denied + - expired + - consumed + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + /workspace/environments: + get: + tags: + - Environments + summary: 'List environments' + description: 'Requires scope `workspace:read` (any role).' + operationId: environments_list + x-action: environments.list + parameters: + - + name: limit + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 50 + - + name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Environment' + meta: + $ref: '#/components/schemas/PageMeta' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Environments + summary: 'Create an environment' + description: | + Requires scope `environments:write` and the `manage-environments` capability + (owner/admin/developer). Danger: critical — with `initial_key` it mints a credential. + + With `initial_key`, the environment's first management key is minted in the same + call and returned once as `initial_key.token` — enough for an agent holding one + workspace key to stand an environment up and start configuring it on the + environment's own host. An idempotent replay returns `initial_key.token: null`. + operationId: environments_create + x-action: environments.create + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + maxLength: 120 + type: + type: string + enum: + - production + - sandbox + default: production + project_id: + type: string + description: "The project (billing anchor) to create the environment under. Defaults to the organization's first project when omitted." + initial_key: + type: + - object + - 'null' + description: "Mint the environment's first management key in the same call." + required: + - name + - scopes + properties: + name: + type: string + maxLength: 120 + scopes: + type: array + minItems: 1 + items: + type: string + description: 'Environment-key scopes, for example `apis:write`.' + expires_at: + type: + - string + - 'null' + format: date-time + responses: + 201: + description: Created + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/CreatedEnvironment' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: 'No such project on this organization' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: not_found + message: 'Project not found.' + 409: + $ref: '#/components/responses/Conflict' + 422: + description: "Validation failed, the project's plan limit is reached, or `initial_key` asks for a scope no management key can carry" + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: environment_limit_reached + message: 'Project [01J…] has reached its plan limit of 2 environment(s).' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/environments/{environment_id}/domain': + delete: + tags: + - Environments + summary: "Stop serving one of the workspace's environments on its custom domain. Anything using that domain stops reaching it." + description: 'Requires scope `environments:write` and a role that may `manage-environments`. Danger: destructive.' + operationId: environments_domain_remove + x-action: environments.domain.remove + x-scope: 'environments:write' + x-danger: destructive + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Environments + summary: "Start serving one of the workspace's environments on a custom domain: returns the DNS TXT record that proves you control it." + description: 'Requires scope `environments:write` and a role that may `manage-environments`. Danger: write.' + operationId: environments_domain_request + x-action: environments.domain.request + x-scope: 'environments:write' + x-danger: write + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + domain: + type: string + description: 'The domain, for example login.example.com.' + maxLength: 253 + required: + - domain + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/CustomDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/environments/{environment_id}/domain/verify': + post: + tags: + - Environments + summary: "Look for the pending domain's DNS TXT record and, once it is visible, serve the environment on that domain." + description: 'Requires scope `environments:write` and a role that may `manage-environments`. Danger: write.' + operationId: environments_domain_verify + x-action: environments.domain.verify + x-scope: 'environments:write' + x-danger: write + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/CustomDomain' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/environments/{environment_id}/keys': + post: + tags: + - Keys + summary: "Mint a management key for one of the workspace's environments, with the scopes it needs. The value is returned once, as `token`." + description: 'Requires scope `keys:write` and a role that may `manage-environments`. Danger: critical.' + operationId: keys_environment_create + x-action: keys.environment.create + x-scope: 'keys:write' + x-danger: critical + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + scopes: + type: array + description: 'The environment-key scopes it carries, for example `apis:write`.' + minItems: 1 + maxItems: 64 + items: + type: string + maxLength: 64 + expires_at: + type: + - string + - 'null' + description: 'When it stops working. Left out, it does not expire.' + format: date-time + required: + - name + - scopes + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EnvironmentKey' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/environments/{environment_id}/keys/{id}': + delete: + tags: + - Keys + summary: "Revoke one of an environment's management keys; whatever uses it stops immediately." + description: 'Requires scope `keys:write` and a role that may `manage-environments`. Danger: destructive.' + operationId: keys_environment_revoke + x-action: keys.environment.revoke + x-scope: 'keys:write' + x-danger: destructive + parameters: + - + name: environment_id + in: path + required: true + schema: + type: string + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /workspace/invitations: + get: + tags: + - Members + summary: 'List pending invitations' + operationId: team_invitations_list + x-action: team.invitations.list + description: "Requires scope `team:read` and the `read-members` capability. The team's invitations nobody has accepted yet, newest first (at most 100)." + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/TeamInvitation' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/invitations/{id}': + delete: + tags: + - Members + summary: 'Withdraw an invitation' + description: 'Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending on this organization is a `404`.' + operationId: team_invitations_revoke + x-action: team.invitations.revoke + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + responses: + 204: + description: Withdrawn + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/invitations/{id}/resend': + post: + tags: + - Members + summary: 'Re-send an invitation' + description: | + Requires scope `team:write` and the `manage-members` capability. Mails a fresh link; the earlier link stops + working and the invitation gets a new `id`. At most once a minute per address + (`429`, `too_soon`). If the mail server refuses, the earlier invitation is kept (`503`). + operationId: team_invitations_resend + x-action: team.invitations.resend + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + responses: + 200: + description: 'Sent again' + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Member' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + description: "Sent less than a minute ago (`too_soon`), or the key's rate limit is exhausted" + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 503: + $ref: '#/components/responses/MailFailed' + /workspace/keys: + post: + tags: + - Keys + summary: 'Mint a workspace key with a role and optional scopes, never wider than the caller. The value is returned once, as `token`.' + description: 'Requires scope `keys:write` and a role that may `manage-environments` and `manage-members`. Danger: critical.' + operationId: keys_workspace_create + x-action: keys.workspace.create + x-scope: 'keys:write' + x-danger: critical + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + role: + type: string + enum: + - admin + - developer + - member + - viewer + scopes: + type: + - array + - 'null' + description: "Narrow the key below its role. Left out, the role alone bounds it — or, minted by a key with scopes, that key's scopes." + minItems: 1 + items: + type: string + enum: + - 'workspace:read' + - 'projects:write' + - 'environments:write' + - 'team:read' + - 'team:write' + - 'keys:write' + - 'settings:write' + expires_at: + type: + - string + - 'null' + description: 'When it stops working. Left out, it does not expire — or, minted by a key that does, expires with it.' + format: date-time + required: + - name + - role + responses: + 201: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/WorkspaceKey' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + get: + tags: + - Keys + summary: "List the workspace's keys, newest first, revoked ones included — names, roles, scopes and expiry, never their values." + description: 'Requires scope `workspace:read` and a role that may `manage-members`. Danger: read.' + operationId: keys_workspace_list + x-action: keys.workspace.list + x-scope: 'workspace:read' + x-danger: read + parameters: + - + name: limit + in: query + required: false + schema: + type: integer + description: 'Items per page, 1–100. Default 50.' + - + name: page + in: query + required: false + schema: + type: integer + description: 'The page to read, from 1. A list says `next_page` when there is one.' + - $ref: '#/components/parameters/CboxApproval' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + $ref: '#/components/schemas/WorkspaceKey' + meta: + $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/keys/{id}': + delete: + tags: + - Keys + summary: 'Revoke a workspace key, and every key it minted; whatever uses them stops immediately.' + description: 'Requires scope `keys:write` and a role that may `manage-environments` and `manage-members`. Danger: destructive.' + operationId: keys_workspace_revoke + x-action: keys.workspace.revoke + x-scope: 'keys:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /workspace/members: + get: + tags: + - Members + summary: 'List members' + operationId: team_list + x-action: team.list + description: 'Requires scope `team:read` and the `read-members` capability (owner/admin/viewer). The roster is PII — a developer key is refused.' + parameters: + - + name: limit + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 50 + - + name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Member' + meta: + $ref: '#/components/schemas/PageMeta' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Members + summary: 'Invite a member' + description: | + Requires scope `team:write` and the `manage-members` capability (owner/admin). Invites the address onto the + organization's team — the same invitation the console's Team page sends: a mail naming + this key as the inviter and the role, with a signed link on which the invitee sets a + password and is signed in. Owner is never invited; ownership is transferred. An + earlier pending invitation for the same address is superseded. List, re-send and + withdraw it under `/workspace/invitations`. + operationId: team_invite + x-action: team.invite + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - role + properties: + email: + type: string + format: email + name: + type: + - string + - 'null' + role: + type: string + enum: + - admin + - developer + - member + - viewer + responses: + 201: + description: Invited + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Member' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + description: 'Validation failed, or the email already belongs to a member' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: email_taken + message: 'That person is already on this list.' + 429: + $ref: '#/components/responses/TooManyRequests' + 503: + $ref: '#/components/responses/MailFailed' + '/workspace/members/{id}': + delete: + tags: + - Members + summary: "Remove a member from the workspace's team. The owner is never removed; transfer ownership first." + description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: destructive.' + operationId: team_remove + x-action: team.remove + x-scope: 'team:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/members/{id}/access': + put: + tags: + - Members + summary: "Set which of the workspace's environments a team member reaches: all of them, or the listed ones." + description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: write.' + operationId: team_environment_access + x-action: team.environment_access + x-scope: 'team:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + all_environments: + type: boolean + description: 'True: every environment, including ones created later.' + environment_ids: + type: array + description: 'With `all_environments` false: the environments they reach.' + items: + type: string + maxLength: 64 + required: + - all_environments + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/members/{id}/role': + patch: + tags: + - Members + summary: "Change a team member's role (admin, developer, member or viewer). The owner's role changes only by transferring ownership." + description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: write.' + operationId: team_role + x-action: team.role + x-scope: 'team:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + role: + type: string + enum: + - admin + - developer + - member + - viewer + required: + - role + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/members/{id}/transfer-ownership': + post: + tags: + - Members + summary: "Make another member the workspace's owner; the current owner stays on as an admin. Only the owner, signed in, may do this." + description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: critical.' + operationId: team_transfer_ownership + x-action: team.transfer_ownership + x-scope: 'team:write' + x-danger: critical + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /workspace/projects: + get: + tags: + - Projects + summary: 'List projects' + operationId: projects_list + x-action: projects.list + description: "Requires scope `workspace:read` (any role). The organization's projects (IdP products). Each carries its own plan and environment allowance." + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Project' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' + post: + tags: + - Projects + summary: 'Create a project' + description: 'Stand up another independently-billed IdP product. Requires scope `projects:write` and the `manage-environments` capability (owner/admin/developer).' + operationId: projects_create + x-action: projects.create + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + maxLength: 120 + environment_limit: + type: integer + minimum: 1 + maximum: 100 + description: "This project's plan environment allowance (default 2)." + responses: + 201: + description: Created + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Project' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /workspace/projects/verification/resend: + post: + tags: + - Projects + summary: "Re-send the signup confirmation the workspace's first environment is waiting on, to the owner's address on file." + description: 'Requires scope `projects:write` and a role that may `manage-environments`. Danger: write.' + operationId: projects_verification_resend + x-action: projects.verification.resend + x-scope: 'projects:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/projects/{id}': + patch: + tags: + - Projects + summary: 'Rename a project.' + description: 'Requires scope `projects:write` and a role that may `manage-environments`. Danger: write.' + operationId: projects_rename + x-action: projects.rename + x-scope: 'projects:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + maxLength: 120 + required: + - name + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Project' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/projects/{id}/reactivate': + post: + tags: + - Projects + summary: 'Reactivate a suspended project, so environments can be added to it again.' + description: 'Requires scope `projects:write` and a role that may `manage-environments`. Danger: write.' + operationId: projects_reactivate + x-action: projects.reactivate + x-scope: 'projects:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Project' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + '/workspace/projects/{id}/suspend': + post: + tags: + - Projects + summary: 'Suspend a project: its environments stay live, but no new ones can be added until it is reactivated.' + description: 'Requires scope `projects:write` and a role that may `manage-environments`. Danger: write.' + operationId: projects_suspend + x-action: projects.suspend + x-scope: 'projects:write' + x-danger: write + parameters: + - + name: id + in: path + required: true + schema: + type: string + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Project' + 202: + $ref: '#/components/responses/ApprovalRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' +components: + securitySchemes: + WorkspaceAccessToken: + type: http + scheme: bearer + bearerFormat: JWT + description: | + A workspace member's access token from the platform root, audienced to the root's + `/mcp`. Bounded by the person's role in the workspace and by its scopes. A DPoP-bound + token is presented as `Authorization: DPoP ` with a proof. + OrganizationApiKey: + type: http + scheme: bearer + description: 'A workspace key, `cbid_ws_…` — a role, optionally narrowed by scopes. Create and revoke keys in the console (Owner/Admin), or with `POST /workspace/keys`.' + parameters: + CboxApproval: + name: Cbox-Approval + in: header + required: false + description: 'The id of an approval this key asked for (from a `202 approval_required`), once the person approved it. Spent by this request, once.' + schema: + type: string + IdempotencyKey: + name: Idempotency-Key + in: header + required: false + description: 'Any unique string (a UUID). A retry with the same key and request gets the first answer back.' + schema: + type: string + maxLength: 255 + responses: + ApprovalRequired: + description: | + Held for a person's approval: this key's policy requires the person who minted it to + approve this action on their device first. Poll `approval.poll_url`, then repeat the + same request (same body, same `Idempotency-Key`) with `Cbox-Approval: `. + content: + application/json: + schema: + type: object + required: + - error + - message + - approval + properties: + error: + type: string + enum: + - approval_required + message: + type: string + approval: + type: object + required: + - id + - status + - binding_code + - expires_at + - poll_url + properties: + id: + type: string + status: + type: string + binding_code: + type: string + expires_at: + type: string + format: date-time + poll_url: + type: string + format: uri + Unauthorized: + description: 'Missing or invalid organization API key (or its organization is suspended)' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Forbidden: + description: "The key's role lacks the required capability, or the key lacks the required scope" + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + MailFailed: + description: 'The mail server refused the invitation (`mail_failed`). A new invitation is withdrawn, so trying again works; a re-sent one is kept.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: 'No such resource on this organization' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Conflict: + description: | + A request with this `Idempotency-Key` is still being processed (`idempotency_in_progress`). + Retry shortly. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UnprocessableEntity: + description: | + Validation failed (`error: validation_failed`, with a field-keyed `errors` map), + or the request was well-formed but refused for a domain reason — a taken email, + a reached plan limit. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + TooManyRequests: + description: | + The key's rate limit for this plane is exhausted. The bucket is keyed on the API + KEY, so this reflects only your own traffic. + headers: + Retry-After: + description: 'Seconds to wait before retrying.' + schema: + type: integer + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Organization: + type: object + properties: + id: + type: string + name: + type: string + status: + type: string + enum: + - active + - suspended + projects: + type: array + description: "The organization's projects with each one's plan/allowance. Present only for billing-readers (plans are per project)." + items: + $ref: '#/components/schemas/Project' + Project: + type: object + properties: + id: + type: string + name: + type: string + slug: + type: string + status: + type: string + enum: + - active + - suspended + environment_limit: + type: integer + description: "This project's plan environment allowance." + environments_used: + type: integer + Environment: + type: object + properties: + id: + type: string + name: + type: string + slug: + type: string + type: + type: string + enum: + - production + - sandbox + status: + type: string + project_id: + type: string + description: 'The project (billing anchor) this environment belongs to.' + domain: + type: + - string + - 'null' + issuer: + type: string + format: uri + description: "The environment's own host — where its OIDC/environment API lives." + Member: + type: object + properties: + id: + type: string + email: + type: string + format: email + name: + type: + - string + - 'null' + role: + type: string + enum: + - owner + - admin + - developer + - member + - viewer + status: + type: string + enum: + - active + - invited + all_environments: + type: boolean + CreatedEnvironment: + description: 'An environment, and — when the request asked for one — its first management key.' + allOf: + - $ref: '#/components/schemas/Environment' + - + type: object + properties: + initial_key: + $ref: '#/components/schemas/EnvironmentKey' + EnvironmentKey: + type: object + description: "A management key (`cbid_env_…`) of one of the workspace's environments. `token` is present only in the answer that minted it, and `null` on an idempotent replay." + required: + - id + - environment_id + - name + - scopes + properties: + id: + type: string + environment_id: + type: string + name: + type: string + prefix: + type: string + scopes: + type: array + items: + type: string + expires_at: + type: + - string + - 'null' + format: date-time + token: + type: + - string + - 'null' + description: "The key's value. Shown once." + WorkspaceKey: + type: object + description: 'A workspace key (`cbid_ws_…`). `token` is present only in the answer that minted it, and `null` on an idempotent replay.' + required: + - id + - name + - role + properties: + id: + type: string + name: + type: string + prefix: + type: string + role: + type: string + enum: + - owner + - admin + - developer + - member + - viewer + scopes: + type: + - array + - 'null' + items: + type: string + enum: + - 'workspace:read' + - 'projects:write' + - 'environments:write' + - 'team:read' + - 'team:write' + - 'keys:write' + - 'settings:write' + description: 'Null when the role alone bounds the key.' + parent_key_id: + type: + - string + - 'null' + description: 'The key that minted it, if a key did. Revoking the parent revokes it.' + created_by_type: + type: + - string + - 'null' + description: '`organization_member` or `workspace_key`; null for a key minted before this was recorded.' + expires_at: + type: + - string + - 'null' + format: date-time + revoked_at: + type: + - string + - 'null' + format: date-time + last_used_at: + type: + - string + - 'null' + format: date-time + token: + type: + - string + - 'null' + description: "The key's value. Shown once." + TeamInvitation: + type: object + required: + - id + - email + - role + - expires_at + properties: + id: + type: string + email: + type: string + format: email + role: + type: string + enum: + - admin + - developer + - member + - viewer + invited_by: + type: + - string + - 'null' + description: 'The person, or the API key, that sent it — by name.' + invited_at: + type: + - string + - 'null' + format: date-time + expires_at: + type: string + format: date-time + CustomDomain: + type: object + required: + - domain + - verified_at + - pending + properties: + domain: + type: + - string + - 'null' + description: 'The verified domain serving the environment.' + verified_at: + type: + - string + - 'null' + format: date-time + pending: + description: 'A domain waiting for its DNS proof, and the TXT record to publish.' + oneOf: + - + type: object + required: + - domain + - record_name + - record_value + properties: + domain: + type: string + record_name: + type: string + record_value: + type: string + - type: 'null' + PageMeta: + type: object + description: 'Paging for a list response. `has_more` used to be returned with nothing to act on it — no page parameter and no cursor — so a caller told there was more had no way to ask for it. `next_page` is present exactly when `has_more` is true, so a client can loop without computing anything.' + properties: + limit: + type: integer + page: + type: integer + minimum: 1 + total: + type: integer + description: 'Present on lists that can count cheaply.' + has_more: + type: boolean + next_page: + type: + - integer + - 'null' + description: 'Pass as `page` on the next request; absent on the last page.' + Error: + type: object + required: + - error + - message + properties: + error: + type: string + description: 'Stable machine code' + message: + type: string + description: 'Human-readable explanation' + request_id: + type: string + description: 'The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem.' + errors: + type: object + description: | + Present on `validation_failed` only: the offending request fields, each + mapped to its messages. + additionalProperties: + type: array + items: + type: string diff --git a/pyproject.toml b/pyproject.toml index 31a202a..eee22c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ dependencies = [ "httpx>=0.27", "pyjwt[crypto]>=2.8", + "typing-extensions>=4.7", ] [project.urls] @@ -36,6 +37,9 @@ dev = [ "pytest>=8.0", "ruff>=0.6", "mypy>=1.11", + # The management-client generator (scripts/generate_management.py) and its drift test. + "pyyaml>=6.0", + "types-PyYAML>=6.0", ] [tool.hatch.build.targets.wheel] @@ -43,15 +47,30 @@ packages = ["src/cbox_id"] [tool.ruff] line-length = 100 -src = ["src", "tests"] +src = ["src", "tests", "."] [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"] +[tool.ruff.format] +# Generated code is compared byte for byte with the generator's output (a test does). +exclude = ["src/cbox_id/management/generated/*"] + +[tool.ruff.lint.per-file-ignores] +# Generated from the OpenAPI documents: descriptions and signatures are kept on one line. +"src/cbox_id/management/generated/*" = ["E501"] + [tool.mypy] python_version = "3.10" strict = true -files = ["src"] +files = [ + "src", + "scripts", + # The management tests assert the generated types (`assert_type`), so they are checked too. + "tests/management_helpers.py", + "tests/test_management.py", + "tests/test_management_audit_logs.py", +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/generate_management.py b/scripts/generate_management.py new file mode 100644 index 0000000..5eb6234 --- /dev/null +++ b/scripts/generate_management.py @@ -0,0 +1,1205 @@ +"""Generate the typed management clients from the OpenAPI documents Cbox ID publishes. + +The specs are vendored in ``openapi/``; the output is ``src/cbox_id/management/generated/``. + + python -m scripts.generate_management regenerate from openapi/*.yaml + python -m scripts.generate_management --fetch environment=https://acme.cboxid.com + refresh one vendored spec from a live host + python -m scripts.generate_management --fetch all=https://id.example.com + refresh all four from one (self-hosted) host + python -m scripts.generate_management --check exit 1 when the generated code is stale + +A small generator on purpose: the output is meant to be read. It knows exactly the subset of +JSON Schema the server's spec builder emits, and fails loudly on anything it does not +understand rather than guessing. The Python twin of id-js's ``scripts/generate-management.ts``. +""" + +from __future__ import annotations + +import argparse +import builtins +import json +import keyword +import re +import sys +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +ROOT = Path(__file__).resolve().parent.parent +SPEC_DIR = ROOT / "openapi" +OUT_DIR = ROOT / "src" / "cbox_id" / "management" / "generated" + +Json = dict[str, Any] + + +@dataclass(frozen=True) +class PlaneConfig: + plane: str + module: str + class_name: str + #: Path the server serves this spec at, relative to the host. + spec_path: str + #: Security schemes that are this plane's management credentials. + schemes: tuple[str, ...] + #: Path prefix stripped when a non-action route's name is derived from its path. + prefix: str + + +PLANES: tuple[PlaneConfig, ...] = ( + PlaneConfig( + plane="environment", + module="environment", + class_name="EnvironmentClient", + spec_path="/api/v1/environment/openapi.yaml", + schemes=("EnvironmentApiKey", "ManagementAccessToken", "WorkspaceAccessToken"), + prefix="", + ), + PlaneConfig( + plane="workspace", + module="workspace", + class_name="WorkspaceClient", + spec_path="/api/v1/workspace/openapi.yaml", + schemes=("OrganizationApiKey", "WorkspaceApiKey", "WorkspaceAccessToken"), + prefix="/workspace", + ), + PlaneConfig( + plane="platform", + module="platform", + class_name="PlatformClient", + spec_path="/api/v1/platform/openapi.yaml", + schemes=("OperatorToken",), + prefix="/platform", + ), + PlaneConfig( + plane="account", + module="account", + class_name="AccountClient", + spec_path="/api/v1/me/openapi.yaml", + schemes=("PersonToken",), + prefix="/me", + ), +) + +METHODS = ("get", "post", "put", "patch", "delete") +WRITES = frozenset({"POST", "PUT", "PATCH", "DELETE"}) +DANGERS = frozenset({"read", "write", "destructive", "critical"}) + +#: What a generated module imports, by source. Only the names a module uses are imported. +IMPORTS: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("collections.abc", ("Iterator", "Mapping")), + ("typing", ("Any", "Literal", "TypeAlias", "overload")), + ("typing_extensions", ("NotRequired", "TypedDict")), + ("..client", ("ManagementClient",)), + ("..models", ("ApiResponse", "ApprovalMode", "OperationSpec", "PendingApprovalResult")), + ("..transport", ("ManagementTransport",)), +) + +#: Names a schema may not take as-is: they would shadow an import or a builtin. +RESERVED = {name for _, names in IMPORTS for name in names} | set(dir(builtins)) + +#: Members every client already has; a namespace may not take these names. +CLIENT_MEMBERS = frozenset({"core", "request", "close", "base_url"}) + +#: Parameter names a generated method already uses. +CALL_PARAMETERS = frozenset( + {"self", "body", "query", "approval", "idempotency_key", "approval_id", "headers"} +) + +#: Names a generated method signature refers to; a method may not shadow them in its class. +SIGNATURE_NAMES = frozenset( + {"str", "None", "Mapping", "Literal", "Iterator", "ApiResponse", "PendingApprovalResult"} + | {"ApprovalMode"} +) + +SIMPLE_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +IDENTIFIER_TOKEN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") + + +class GeneratorError(Exception): + """The spec uses something this generator does not understand.""" + + +class _SpecLoader(yaml.SafeLoader): + """YAML 1.2 scalars: only ``true``/``false`` are booleans, and dates stay strings. + + PyYAML follows YAML 1.1, where an unquoted ``on``, ``no`` or ``2026-10-08`` would turn an + enum value or an example into something the server never meant. + """ + + +_SpecLoader.yaml_implicit_resolvers = { + first: [ + (tag, regexp) + for tag, regexp in resolvers + if tag not in ("tag:yaml.org,2002:bool", "tag:yaml.org,2002:timestamp") + ] + for first, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items() +} +_SpecLoader.add_implicit_resolver( + "tag:yaml.org,2002:bool", re.compile(r"^(?:true|True|TRUE|false|False|FALSE)$"), list("tTfF") +) + + +def load_spec(text: str) -> Json: + parsed = yaml.load(text, Loader=_SpecLoader) # noqa: S506 - a SafeLoader subclass + + if not isinstance(parsed, dict): + raise GeneratorError("not a YAML mapping") + + return parsed + + +def obj(value: object) -> Json: + return value if isinstance(value, dict) else {} + + +def arr(value: object) -> list[Any]: + return value if isinstance(value, list) else [] + + +def text(value: object) -> str | None: + return value if isinstance(value, str) else None + + +def names_in(expression: str) -> set[str]: + """The names a type expression refers to — not the words inside its string literals.""" + return set(IDENTIFIER_TOKEN.findall(re.sub(r'"(?:[^"\\]|\\.)*"', "", expression))) + + +def pascal(parts: Iterable[str]) -> str: + out = "" + + for part in parts: + for segment in re.split(r"[^A-Za-z0-9]+", part): + if segment: + out += segment[:1].upper() + segment[1:] + + return out + + +def identifier(name: str) -> str: + """A Python identifier for a name from the spec: ``import`` → ``import_``.""" + out = re.sub(r"\W", "_", name) + + if out[:1].isdigit(): + out = f"_{out}" + + return f"{out}_" if keyword.iskeyword(out) else out + + +def literal(value: object) -> str: + if isinstance(value, bool): + return "True" if value else "False" + + if value is None: + return "None" + + if isinstance(value, str): + return json.dumps(value, ensure_ascii=False) + + if isinstance(value, (int, float)): + return repr(value) + + raise GeneratorError(f"unsupported literal {value!r}") + + +def _escape_doc(line: str) -> str: + return line.replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + + +def docstring(text: str | None, indent: str, extra: Sequence[str] = ()) -> str: + lines = [line.rstrip() for line in (text or "").strip().split("\n")] + lines = [_escape_doc(line) for line in [*lines, *extra]] + + while lines and lines[0] == "": + lines.pop(0) + while lines and lines[-1] == "": + lines.pop() + + if not lines: + return "" + + if len(lines) == 1 and not lines[0].endswith('"'): + return f'{indent}"""{lines[0]}"""\n' + + body = "\n".join(f"{indent}{line}" if line else "" for line in lines[1:]) + rest = f"{body}\n" if body else "" + + return f'{indent}"""{lines[0]}\n{rest}{indent}"""\n' + + +def comment(text: str | None, indent: str) -> str: + lines = [line.rstrip() for line in (text or "").strip().split("\n")] + + if lines == [""]: + return "" + + return "".join(f"{indent}#: {line}".rstrip() + "\n" for line in lines) + + +@dataclass +class Operation: + name: list[str] + key: str + action: str | None + operation_id: str | None + method: str + path: str + path_params: list[str] + summary: str | None + description: str | None + scope: str | None + danger: str | None + approval: bool + input_kind: str | None + input_required: bool + input_schema: Json | None + response_schema: Json | None + pagination: str | None + + +@dataclass +class _Node: + children: dict[str, _Node] = field(default_factory=dict) + ops: list[Operation] = field(default_factory=list) + + +class Generator: + def __init__(self, spec: Json, config: PlaneConfig) -> None: + self.spec = spec + self.config = config + self.blocks: list[str] = [] + self.used: set[str] = set() + self.imports: set[str] = set() + self.type_names: dict[str, str] = {} + self.object_components: set[str] = set() + self.generated_types: set[str] = set() + self.skipped: list[str] = [] + + for name, schema in self.schemas().items(): + safe = f"{name}Schema" if name in RESERVED else name + + if not SIMPLE_NAME.match(safe) or keyword.iskeyword(safe): + raise GeneratorError(f"{config.module}: schema name {name!r} is not an identifier") + + self.type_names[name] = safe + self.used.add(safe) + self.generated_types.add(safe) + + if self._is_object(schema): + self.object_components.add(name) + + # ── Spec access ────────────────────────────────────────────────────────────────────── + + def components(self, kind: str) -> Json: + components = self.spec.get("components") + section = components.get(kind) if isinstance(components, dict) else None + return section if isinstance(section, dict) else {} + + def schemas(self) -> Json: + return self.components("schemas") + + def deref(self, value: object, kind: str) -> Json: + if isinstance(value, dict) and isinstance(value.get("$ref"), str): + name = value["$ref"].split("/")[-1] + target = self.components(kind).get(name) + + if not isinstance(target, dict): + raise GeneratorError(f"{self.config.module}: unresolved {value['$ref']}") + + return target + + if not isinstance(value, dict): + raise GeneratorError(f"{self.config.module}: expected an object, got {value!r}") + + return value + + def _is_object(self, schema: object) -> bool: + if not isinstance(schema, dict): + return False + + if "allOf" in schema: + return True + + return bool(schema.get("properties")) and schema.get("type", "object") == "object" + + # ── Names ──────────────────────────────────────────────────────────────────────────── + + def claim(self, name: str) -> str: + if name in self.used or name in RESERVED: + raise GeneratorError(f"{self.config.module}: generated name {name} collides") + + self.used.add(name) + self.generated_types.add(name) + return name + + def use(self, *names: str) -> None: + self.imports.update(names) + + # ── Types ──────────────────────────────────────────────────────────────────────────── + + def type_of(self, schema: object, hint: str) -> str: + """A JSON Schema as a Python type expression; inline objects become named TypedDicts.""" + if schema is None or schema is True or schema == {}: + self.use("Any") + return "Any" + + if not isinstance(schema, dict): + raise GeneratorError(f"{self.config.module}: unsupported schema {schema!r}") + + if isinstance(schema.get("$ref"), str): + name = schema["$ref"].split("/")[-1] + + if name not in self.type_names: + raise GeneratorError(f"{self.config.module}: unresolved {schema['$ref']}") + + return self.type_names[name] + + if "const" in schema: + self.use("Literal") + return f"Literal[{literal(schema['const'])}]" + + for key in ("oneOf", "anyOf"): + if isinstance(schema.get(key), list): + return self.union( + [self.type_of(s, f"{hint}Option{i + 1}") for i, s in enumerate(schema[key])] + ) + + if "allOf" in schema: + name = self.claim(hint) + self.typed_dict(name, schema) + return name + + raw_type = schema.get("type") + types: list[str] = ( + list(raw_type) + if isinstance(raw_type, list) + else [raw_type] + if isinstance(raw_type, str) + else [] + ) + + if isinstance(schema.get("enum"), list): + values = [literal(v) for v in schema["enum"] if v is not None] + self.use("Literal") + out = f"Literal[{', '.join(values)}]" + nullable = "null" in types or None in schema["enum"] + return f"{out} | None" if nullable else out + + if not types: + if schema.get("properties"): + return self.object_type(schema, hint) + + self.use("Any") + return "Any" + + return self.union([self.primitive(t, schema, hint) for t in types]) + + def union(self, parts: list[str]) -> str: + unique = list(dict.fromkeys(parts)) + + if "Any" in unique: + return "Any" + + # `None` last reads as "optional". + unique.sort(key=lambda part: part == "None") + return " | ".join(unique) + + def primitive(self, kind: str, schema: Json, hint: str) -> str: + if kind == "string": + return "str" + if kind == "integer": + return "int" + if kind == "number": + return "float" + if kind == "boolean": + return "bool" + if kind == "null": + return "None" + if kind == "array": + return f"list[{self.type_of(schema.get('items'), f'{hint}Item')}]" + if kind == "object": + return self.object_type(schema, hint) + + raise GeneratorError(f"{self.config.module}: unsupported type {kind}") + + def object_type(self, schema: Json, hint: str) -> str: + properties = schema.get("properties") + additional = schema.get("additionalProperties") + + if not properties: + if additional is None or additional is False or additional is True: + self.use("Any") + return "dict[str, Any]" + + return f"dict[str, {self.type_of(additional, f'{hint}Value')}]" + + name = self.claim(hint) + self.typed_dict(name, schema) + return name + + def typed_dict(self, name: str, schema: Json) -> None: + """Emit a TypedDict for an object schema (or an ``allOf`` of them).""" + bases: list[str] = [] + parts: list[Json] = [schema] + + if "allOf" in schema: + parts = [] + + for part in schema["allOf"]: + if isinstance(part, dict) and isinstance(part.get("$ref"), str): + ref = part["$ref"].split("/")[-1] + + if ref not in self.object_components: + raise GeneratorError( + f"{self.config.module}: {name} extends {ref}, which is not an object" + ) + + bases.append(self.type_names[ref]) + elif self._is_object(part) or (isinstance(part, dict) and "properties" in part): + parts.append(part) + else: + raise GeneratorError(f"{self.config.module}: unsupported allOf part in {name}") + + if schema.get("properties") and schema.get("additionalProperties") not in (None, False): + raise GeneratorError( + f"{self.config.module}: {name} has both properties and additionalProperties" + ) + + fields: list[tuple[str, str, str | None]] = [] + + for part in parts: + required = set(part.get("required") or []) + + for key, value in (part.get("properties") or {}).items(): + annotation = self.type_of(value, f"{name}{pascal([key])}") + + if key not in required: + self.use("NotRequired") + annotation = f"NotRequired[{annotation}]" + + description = value.get("description") if isinstance(value, dict) else None + fields.append((key, annotation, description)) + + self.use("TypedDict") + description = schema.get("description") + tokens = {t for _, annotation, _ in fields for t in names_in(annotation)} + functional = any( + not SIMPLE_NAME.match(key) or keyword.iskeyword(key) or key in tokens + for key, _, _ in fields + ) + + if functional: + if bases: + raise GeneratorError(f"{self.config.module}: {name} cannot be both") + + # Keys that are not identifiers, or that would shadow a type the class body uses. + entries = ", ".join(f"{literal(k)}: {a!r}" for k, a, _ in fields) + block = f'{name} = TypedDict("{name}", {{{entries}}})\n' + self.blocks.append(f"{block}{docstring(description, '')}") + return + + lines = [f"class {name}({', '.join(bases) if bases else 'TypedDict'}):"] + doc = docstring(description, " ") + + if doc: + lines.append(doc.rstrip("\n")) + + if fields: + lines.append("") + + for key, annotation, field_doc in fields: + note = comment(field_doc, " ") + + if note: + # A described field gets air above it, so its comment reads as its own. + if len(lines) > 1: + lines.append("") + lines.append(note.rstrip("\n")) + + lines.append(f" {key}: {annotation}") + + if len(lines) == 1: + lines.append(" pass") + + self.blocks.append("\n".join(lines) + "\n") + + def alias(self, name: str, expression: str, description: str | None = None) -> None: + self.use("TypeAlias") + # Quoted only when it names a generated type, which may be defined further down. + forward = names_in(expression) & self.generated_types + value = repr(expression) if forward else expression + self.blocks.append(f"{comment(description, '')}{name}: TypeAlias = {value}\n") + + def named(self, expression: str, name: str, description: str | None = None) -> str: + """``expression`` when it already is a name; else a new alias ``name`` for it.""" + if expression in self.generated_types or expression in ("Any", "None"): + return expression + + self.alias(self.claim(name), expression, description) + return name + + # ── Operations ─────────────────────────────────────────────────────────────────────── + + def operations(self) -> list[Operation]: + paths = obj(self.spec.get("paths")) + default_security = arr(self.spec.get("security")) + ops: list[Operation] = [] + + for path, raw_item in paths.items(): + item = self.deref(raw_item, "pathItems") + shared = arr(item.get("parameters")) + + for method in METHODS: + op = item.get(method) + + if not isinstance(op, dict): + continue + + security = arr(op["security"]) if "security" in op else default_security + schemes = [name for entry in security if isinstance(entry, dict) for name in entry] + action = text(op.get("x-action")) + + if not any(scheme in self.config.schemes for scheme in schemes): + listed = ", ".join(schemes) or "no security" + self.skipped.append(f"{method.upper()} {path} ({listed})") + continue + + own = arr(op.get("parameters")) + parameters = [self.deref(p, "parameters") for p in [*shared, *own]] + path_params = re.findall(r"\{([^}]+)\}", path) + query = [p for p in parameters if p.get("in") == "query"] + description = text(op.get("description")) + # `x-scope` / `x-danger` are the contract. Routes the spec builder does not + # generate from an action carry neither, and only say it in prose. + scope = text(op.get("x-scope")) + danger = text(op.get("x-danger")) + + if scope is None and description is not None: + found = re.search(r"Requires scope `([^`]+)`", description) + scope = found.group(1) if found else None + + if danger is None and description is not None: + found = re.search(r"Danger: ([a-z]+)", description) + danger = found.group(1) if found else None + + if danger is not None and danger not in DANGERS: + raise GeneratorError( + f"{self.config.module}: {method} {path} has an unknown danger {danger}" + ) + + raw_responses = obj(op.get("responses")) + responses = {str(code): value for code, value in raw_responses.items()} + input_kind: str | None = None + input_schema: Json | None = None + input_required = False + + if op.get("requestBody") is not None: + body = self.deref(op["requestBody"], "requestBodies") + content = obj(body.get("content")) + json_body = content.get("application/json") + + if not isinstance(json_body, dict): + raise GeneratorError( + f"{self.config.module}: {method} {path} has a non-JSON body" + ) + + if query: + raise GeneratorError( + f"{self.config.module}: {method} {path} has a body and a query" + ) + + input_kind = "body" + raw_schema = json_body.get("schema") + input_schema = raw_schema if isinstance(raw_schema, dict) else {} + required_props = len(input_schema.get("required") or []) + input_required = body.get("required") is True and required_props > 0 + elif query: + input_kind = "query" + properties: Json = {} + + for param in query: + param_schema = obj(param.get("schema")) + if isinstance(param.get("description"), str): + param_schema = {**param_schema, "description": param["description"]} + properties[param["name"]] = param_schema + + input_schema = { + "type": "object", + "properties": properties, + "required": [p["name"] for p in query if p.get("required") is True], + } + input_required = any(p.get("required") is True for p in query) + + response_schema: Json | None = None + + for code in ("200", "201", "204"): + if code not in responses: + continue + + response = self.deref(responses[code], "responses") + content = obj(response.get("content")) + json_response = content.get("application/json") + schema = ( + json_response.get("schema") if isinstance(json_response, dict) else None + ) + response_schema = schema if isinstance(schema, dict) else None + break + + query_names = {p.get("name") for p in query} + name = action.split(".") if action is not None else self.derived_name(method, path) + + ops.append( + Operation( + name=name, + key=action if action is not None else ".".join(name), + action=action, + operation_id=op.get("operationId") + if isinstance(op.get("operationId"), str) + else None, + method=method.upper(), + path=path, + path_params=path_params, + summary=text(op.get("summary")), + description=description, + scope=scope, + danger=danger, + approval="202" in responses and self.is_approval(responses["202"]), + input_kind=input_kind, + input_required=input_required, + input_schema=input_schema, + response_schema=response_schema, + pagination="cursor" + if "after" in query_names + else "page" + if "page" in query_names + else None, + ) + ) + + # The account and platform planes name every action `account.…` / `platform.…`. The + # client already says which plane it is, so `me.sessions.revoke_others()` rather than + # `me.account.sessions.revoke_others()`. Only when EVERY action shares the prefix. + actions = [o for o in ops if o.action is not None] + + if actions and all(o.name[0] == self.config.plane and len(o.name) > 2 for o in actions): + for o in actions: + o.name = o.name[1:] + + for o in ops: + o.name = [identifier(segment) for segment in o.name] + + return sorted(ops, key=lambda o: ".".join(o.name)) + + def is_approval(self, response: object) -> bool: + if isinstance(response, dict) and isinstance(response.get("$ref"), str): + return bool(response["$ref"].endswith("/ApprovalRequired")) + + return "approval_required" in json.dumps(response) + + def derived_name(self, method: str, path: str) -> list[str]: + """A name for a route that is not an action: ``GET /apis/{id}`` → ``apis.get``.""" + prefix = self.config.prefix + rest = path[len(prefix) :] if prefix and path.startswith(f"{prefix}/") else path + segments = [s for s in rest.split("/") if s] + resources = [s.replace("-", "_") for s in segments if not s.startswith("{")] + on_item = bool(segments) and segments[-1].startswith("{") + verb = { + "get": "get" if on_item else "list", + "post": "create", + "put": "set", + "patch": "update", + "delete": "delete", + }[method] + + return [*resources, verb] + + # ── Rendering ──────────────────────────────────────────────────────────────────────── + + def render(self) -> tuple[str, list[Operation]]: + config = self.config + info = obj(self.spec.get("info")) + title = text(info.get("title")) or config.class_name + operations = self.operations() + table = f"{config.plane.upper()}_OPERATIONS" + self.claim(table) + self.claim(config.class_name) + + # ── Schemas + self.blocks.append(f"# ── Schemas (components.schemas) {'─' * 66}\n") + + for name, schema in self.schemas().items(): + type_name = self.type_names[name] + description = schema.get("description") if isinstance(schema, dict) else None + + if name in self.object_components: + self.typed_dict(type_name, schema) + else: + expression = self.type_of(schema, f"{type_name}Value") + self.alias(type_name, expression, description) + + # ── Per-operation types + self.blocks.append(f"# ── Operation inputs and responses {'─' * 64}\n") + signatures: dict[str, tuple[str | None, str, str | None]] = {} + + for op in operations: + base = pascal(op.name) + input_type: str | None = None + + if op.input_kind is not None and op.input_schema is not None: + suffix = "Body" if op.input_kind == "body" else "Query" + hint = f"{base}{suffix}" + expression = self.type_of(op.input_schema, hint) + input_type = expression if expression == hint else self.named(expression, hint) + + data_type, item_type = self.response_types(op, base) + signatures[op.key] = (input_type, data_type, item_type) + + # ── Operation table + self.blocks.append(f"# ── Operations {'─' * 84}\n") + self.use("Mapping", "OperationSpec") + keys = [op.key for op in operations] + + if len(set(keys)) != len(keys): + raise GeneratorError(f"{config.module}: two operations share a key") + + rows = [comment(f"Every operation of the {title}, keyed by action name.", "").rstrip()] + rows.append(f"{table}: Mapping[str, OperationSpec] = {{") + + for op in operations: + params = ", ".join(literal(p) for p in op.path_params) + path_params = f"({params},)" if len(op.path_params) == 1 else f"({params})" + spec = ", ".join( + [ + f"action={literal(op.action)}", + f"operation_id={literal(op.operation_id)}", + f"method={literal(op.method)}", + f"path={literal(op.path)}", + f"path_params={path_params}", + f"scope={literal(op.scope)}", + f"danger={literal(op.danger)}", + f"approval={literal(op.approval)}", + f"body={literal(op.input_kind == 'body')}", + f"pagination={literal(op.pagination)}", + ] + ) + rows.append(f" {literal(op.key)}: OperationSpec({spec}),") + + rows.append("}\n") + self.blocks.append("\n".join(rows)) + + # ── Namespaces + self.blocks.append(f"# ── Namespaces {'─' * 84}\n") + root = _Node() + + for op in operations: + node = root + + for segment in op.name[:-1]: + node = node.children.setdefault(segment, _Node()) + + node.ops.append(op) + + if root.ops: + names = ", ".join(".".join(o.name) for o in root.ops) + raise GeneratorError( + f"{config.module}: an operation has a single-segment name: {names}" + ) + + for segment in root.children: + if segment in CLIENT_MEMBERS: + raise GeneratorError( + f"{config.module}: namespace {segment} shadows a client member" + ) + + self.use("ManagementTransport") + + for path, node in self.walk(root, []): + self.namespace(path, node, table, signatures) + + # ── Client + self.blocks.append(f"# ── Client {'─' * 88}\n") + self.use("ManagementClient") + servers = arr(self.spec.get("servers")) + fixed = next( + ( + s["url"] + for s in servers + if isinstance(s, dict) and isinstance(s.get("url"), str) and "{" not in s["url"] + ), + None, + ) + default_base = re.sub(r"/api/v1/?$", "", fixed) if fixed is not None else None + raw_description = text(info.get("description")) or "" + intro = raw_description.split("\n\n")[0] + base_note = ( + f"``base_url`` defaults to ``{default_base}``." + if default_base is not None + else "``base_url`` is required: the environment's own host." + ) + lines = [f"class {config.class_name}(ManagementClient):"] + lines.append(docstring(f"{title}.\n\n{intro}\n\n{base_note}", " ").rstrip("\n")) + lines.append("") + lines.append(f" _plane = {literal(config.plane)}") + + if default_base is not None: + lines.append(f" _default_base_url = {literal(default_base)}") + + lines.append("") + lines.append(" def _bind(self, core: ManagementTransport) -> None:") + + for segment in root.children: + lines.append(f" self.{segment} = {self.namespace_class([segment])}(core)") + + self.blocks.append("\n".join(lines) + "\n") + + header = ( + f"# GENERATED by scripts/generate_management.py from openapi/{config.module}.yaml" + " — do not edit.\n" + "# Regenerate with `python -m scripts.generate_management`.\n" + ) + module_doc = f'"""{_escape_doc(title)}: typed client, schemas and operation table."""\n' + imports = ["from __future__ import annotations", ""] + group = "" + + for module, imported in IMPORTS: + used = [n for n in imported if n in self.imports] + + if not used: + continue + + # isort: stdlib, then third party, then local — a blank line between groups. + kind = "local" if module.startswith(".") else "third" if "_" in module else "std" + + if group and kind != group: + imports.append("") + + group = kind + imports.append(f"from {module} import {', '.join(used)}") + + code = "\n".join([header + module_doc, *imports, "", ""]) + "\n\n".join(self.blocks) + code = re.sub(r"\n{3,}", "\n\n\n", code).rstrip() + "\n" + + return code, operations + + def response_types(self, op: Operation, base: str) -> tuple[str, str | None]: + """The type of ``data`` in the response, and of one item when the list is paged.""" + schema = op.response_schema + + if schema is None: + return "None", None + + properties = obj(schema.get("properties")) + data = properties.get("data") if "data" in properties else schema + hint = f"{base}Data" + item_type: str | None = None + + if isinstance(data, dict) and data.get("type") == "array": + item = self.type_of(data.get("items"), f"{hint}Item") + expression = f"list[{item}]" + + if op.pagination is not None: + item_type = self.named(item, f"{base}Item") + else: + if op.pagination is not None: + raise GeneratorError(f"{self.config.module}: paged {op.key} answers no data array") + + expression = self.type_of(data, hint) + + if expression == hint: + return expression, item_type + + return self.named(expression, hint), item_type + + def walk(self, node: _Node, path: list[str]) -> list[tuple[list[str], _Node]]: + """Every namespace below the root, children before their parent.""" + out: list[tuple[list[str], _Node]] = [] + + for segment, child in node.children.items(): + out.extend(self.walk(child, [*path, segment])) + out.append(([*path, segment], child)) + + return out + + def namespace_class(self, path: list[str]) -> str: + return f"{pascal(path)}Methods" + + def namespace( + self, + path: list[str], + node: _Node, + table: str, + signatures: dict[str, tuple[str | None, str, str | None]], + ) -> None: + name = self.claim(self.namespace_class(path)) + members: set[str] = set() + lines = [f"class {name}:"] + lines.append(f' """``{".".join(path)}.*``"""') + lines.append("") + lines.append(" def __init__(self, core: ManagementTransport) -> None:") + lines.append(" self._core = core") + + for segment in node.children: + members.add(segment) + lines.append(f" self.{segment} = {self.namespace_class([*path, segment])}(core)") + + def member(member_name: str, where: str) -> str: + if ( + member_name in members + or member_name.startswith("_") + or member_name in SIGNATURE_NAMES + ): + raise GeneratorError( + f"{self.config.module}: member {member_name} collides at {where}" + ) + + if member_name in self.generated_types: + raise GeneratorError(f"{self.config.module}: member {member_name} shadows a type") + + members.add(member_name) + return member_name + + for op in node.ops: + leaf = member(op.name[-1], ".".join(op.name)) + input_type, data_type, item_type = signatures[op.key] + lines.append("") + lines.extend(self.method(op, leaf, table, input_type, data_type)) + + if op.pagination is not None and item_type is not None: + all_name = member(f"{leaf}_all", ".".join(op.name) + "_all") + lines.append("") + lines.extend(self.paged_method(op, all_name, table, input_type, item_type)) + + self.blocks.append("\n".join(lines) + "\n") + + def _parameters(self, op: Operation, input_type: str | None) -> tuple[list[str], list[str]]: + params: list[str] = [] + args: list[str] = [] + + for raw in op.path_params: + param = identifier(raw) + + if param in CALL_PARAMETERS: + raise GeneratorError(f"{self.config.module}: path parameter {raw} collides") + + params.append(f"{param}: str") + args.append(param) + + if input_type is not None: + arg = "body" if op.input_kind == "body" else "query" + params.append( + f"{arg}: {input_type}" + if op.input_required + else f"{arg}: {input_type} | None = None" + ) + + return params, args + + def method( + self, op: Operation, leaf: str, table: str, input_type: str | None, data_type: str + ) -> list[str]: + params, args = self._parameters(op, input_type) + write = op.method in WRITES + path_args = f"({args[0]},)" if len(args) == 1 else f"({', '.join(args)})" + input_arg = ( + "None" if input_type is None else ("body" if op.input_kind == "body" else "query") + ) + response = f"ApiResponse[{data_type}]" + self.use("ApiResponse", "Mapping") + headers = "headers: Mapping[str, str] | None = None" + key_param = "idempotency_key: str | None = None" + meta = [f"``{op.method} {op.path}``" + (f" · action ``{op.action}``" if op.action else "")] + + if op.scope is not None or op.danger is not None: + facts = [] + if op.scope is not None: + facts.append(f"Scope ``{op.scope}``") + if op.danger is not None: + facts.append(f"danger: {op.danger}") + meta.append(" · ".join(facts) + ".") + + if op.approval: + meta.append( + "May be held for approval (``202 approval_required``): waited on unless" + ' ``approval="return"``.' + ) + + details = f"\n\n{op.description}" if op.description and op.description.strip() else "" + doc = docstring(f"{op.summary or ''}{details}", " ", ["", *meta]) + lines: list[str] = [] + + if op.approval: + self.use("overload", "Literal", "PendingApprovalResult", "ApprovalMode") + pending = f"{response} | PendingApprovalResult[{data_type}]" + shared = [key_param] if write else [] + shared += ["approval_id: str | None = None", headers] + wait = ", ".join(["self", *params, "*", 'approval: Literal["wait"] = "wait"', *shared]) + give = ", ".join(["self", *params, "*", 'approval: Literal["return"]', *shared]) + impl = ", ".join(["self", *params, "*", 'approval: ApprovalMode = "wait"', *shared]) + call_kwargs = ["approval=approval"] + call_kwargs += ["idempotency_key=idempotency_key"] if write else [] + call_kwargs += ["approval_id=approval_id", "headers=headers"] + lines += [ + " @overload", + f" def {leaf}({wait}) -> {response}: ...", + "", + " @overload", + f" def {leaf}({give}) -> {pending}: ...", + "", + f" def {leaf}({impl}) -> {pending}:", + ] + else: + shared = [key_param] if write else [] + shared += [headers] + signature = ", ".join(["self", *params, "*", *shared]) + call_kwargs = ["idempotency_key=idempotency_key"] if write else [] + call_kwargs += ["headers=headers"] + lines.append(f" def {leaf}({signature}) -> {response}:") + + lines.append(doc.rstrip("\n")) + lines.append( + f" return self._core.call({table}[{literal(op.key)}], {path_args}, {input_arg}," + f" {', '.join(call_kwargs)})" + ) + + return lines + + def paged_method( + self, op: Operation, name: str, table: str, input_type: str | None, item_type: str + ) -> list[str]: + self.use("Iterator", "Mapping") + params, args = self._parameters(op, None) + query = f"query: {input_type} | None = None" if input_type is not None else None + signature = ", ".join( + [ + "self", + *params, + *([query] if query else []), + "*", + "headers: Mapping[str, str] | None = None", + ] + ) + path_args = f"({args[0]},)" if len(args) == 1 else f"({', '.join(args)})" + query_arg = "query" if input_type is not None else "None" + dotted = ".".join(op.name) + doc = docstring( + f"Every item of ``{dotted}``, fetching pages as the iteration reaches them.", " " + ) + + return [ + f" def {name}({signature}) -> Iterator[{item_type}]:", + doc.rstrip("\n"), + f" return self._core.paginate({table}[{literal(op.key)}], {path_args}," + f" {query_arg}, headers=headers)", + ] + + +@dataclass(frozen=True) +class Result: + code: str + operations: int + skipped: list[str] + + +def generate_plane(spec: Json, config: PlaneConfig) -> Result: + """Generate one plane's client module from its parsed spec.""" + version = spec.get("openapi") + + if not isinstance(version, str) or not version.startswith("3."): + raise GeneratorError(f"{config.module}: not an OpenAPI 3 document") + + generator = Generator(spec, config) + code, operations = generator.render() + + return Result(code=code, operations=len(operations), skipped=generator.skipped) + + +def generate_all(root: Path = ROOT) -> dict[Path, Result]: + """Generate every plane from the vendored specs in ``openapi/``, keyed by output path.""" + results: dict[Path, Result] = {} + + for config in PLANES: + spec = load_spec((root / "openapi" / f"{config.module}.yaml").read_text("utf-8")) + out = root / "src" / "cbox_id" / "management" / "generated" / f"{config.module}.py" + results[out] = generate_plane(spec, config) + + return results + + +def fetch_spec(config: PlaneConfig, target: str) -> None: + import httpx + + url = ( + target if target.endswith((".yaml", ".json")) else f"{target.rstrip('/')}{config.spec_path}" + ) + response = httpx.get(url, headers={"Accept": "application/yaml, application/json"}) + + if response.status_code != 200: + raise GeneratorError(f"GET {url} answered {response.status_code}") + + parsed = load_spec(response.text) + + if not isinstance(parsed.get("openapi"), str): + raise GeneratorError(f"GET {url} did not return an OpenAPI document") + + (SPEC_DIR / f"{config.module}.yaml").write_text(response.text, "utf-8") + print(f"fetched {config.module} <- {url}") + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="python -m scripts.generate_management") + parser.add_argument("--check", action="store_true", help="exit 1 when generated code is stale") + parser.add_argument( + "--fetch", + action="append", + default=[], + metavar="PLANE=URL", + help="refresh a vendored spec from a running server first (PLANE may be `all`)", + ) + options = parser.parse_args(argv) + + for target in options.fetch: + plane, _, url = target.partition("=") + + if not plane or not url: + parser.error( + "--fetch takes plane=url, e.g. --fetch environment=https://acme.cboxid.com" + ) + + configs = [p for p in PLANES if plane in ("all", p.plane)] + + if not configs: + parser.error(f"unknown plane {plane}; use {', '.join(p.plane for p in PLANES)} or all") + + for config in configs: + fetch_spec(config, url) + + stale = False + + for path, result in generate_all().items(): + current = path.read_text("utf-8") if path.exists() else "" + + if options.check: + if current != result.code: + print(f"stale: {path.relative_to(ROOT)}", file=sys.stderr) + stale = True + continue + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(result.code, "utf-8") + skipped = f", skipped {len(result.skipped)} (other credentials)" if result.skipped else "" + print(f"{path.relative_to(ROOT)}: {result.operations} operations{skipped}") + + if stale: + print("Run `python -m scripts.generate_management` and commit the result.", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 93747cecb42a04a5e9c1d12c61542b38b4e098c5 Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 10:27:49 +0200 Subject: [PATCH 2/8] feat(management): typed clients for the management planes EnvironmentClient, WorkspaceClient, PlatformClient and AccountClient, generated from the vendored specs and named after the server's actions. A hand-written transport carries the credential checks, Idempotency-Key reuse across retries (Retry-After honoured), the approval loop with same-origin polling and a return-pending mode, Cbox-Environment for root tokens, DPoP proofs, paging, and CboxIdApiError / ManagementNetworkError. Mirrors id-js's management client. Includes the Audit Logs helpers (buffered logger, export poller, chain verifier byte-compatible with the server's canonical JSON). --- src/cbox_id/management/__init__.py | 115 + src/cbox_id/management/audit_logs.py | 523 ++ src/cbox_id/management/client.py | 161 + src/cbox_id/management/dpop.py | 95 + src/cbox_id/management/errors.py | 106 + src/cbox_id/management/generated/__init__.py | 1 + src/cbox_id/management/generated/account.py | 373 + .../management/generated/environment.py | 7176 +++++++++++++++++ src/cbox_id/management/generated/platform.py | 408 + src/cbox_id/management/generated/workspace.py | 861 ++ src/cbox_id/management/models.py | 135 + src/cbox_id/management/transport.py | 798 ++ tests/fixtures/management_surface.txt | 282 + tests/management_helpers.py | 95 + tests/test_management.py | 714 ++ 15 files changed, 11843 insertions(+) create mode 100644 src/cbox_id/management/__init__.py create mode 100644 src/cbox_id/management/audit_logs.py create mode 100644 src/cbox_id/management/client.py create mode 100644 src/cbox_id/management/dpop.py create mode 100644 src/cbox_id/management/errors.py create mode 100644 src/cbox_id/management/generated/__init__.py create mode 100644 src/cbox_id/management/generated/account.py create mode 100644 src/cbox_id/management/generated/environment.py create mode 100644 src/cbox_id/management/generated/platform.py create mode 100644 src/cbox_id/management/generated/workspace.py create mode 100644 src/cbox_id/management/models.py create mode 100644 src/cbox_id/management/transport.py create mode 100644 tests/fixtures/management_surface.txt create mode 100644 tests/management_helpers.py create mode 100644 tests/test_management.py diff --git a/src/cbox_id/management/__init__.py b/src/cbox_id/management/__init__.py new file mode 100644 index 0000000..1ba0f5f --- /dev/null +++ b/src/cbox_id/management/__init__.py @@ -0,0 +1,115 @@ +"""Typed clients for Cbox ID's management planes. + +Generated from the OpenAPI documents the server publishes (see ``openapi/`` and +``python -m scripts.generate_management``): + +- :class:`EnvironmentClient`: one environment's tenancy, on its own host (``cbid_env_…`` + key or a delegated access token). +- :class:`WorkspaceClient`: the workspace above its environments (``cbid_ws_…`` key). +- :class:`PlatformClient`: the deployment itself, for operators (delegated token only). +- :class:`AccountClient`: a person's own account (delegated token only). + +Server-side code only: every client holds a management credential. Schema and operation +types live in the plane's module, e.g. ``cbox_id.management.generated.environment.App``. +""" + +from __future__ import annotations + +from ..errors import CboxIdError, ConfigurationError +from .audit_logs import ( + AUDIT_CHAIN_GENESIS, + MAX_AUDIT_BATCH, + AuditChainVerification, + AuditLogEventInput, + AuditLogExportError, + AuditLogger, + AuditLogRecord, + audit_event_document, + audit_event_hash, + canonical_json, + export_audit_logs, + verify_audit_chain, + verify_audit_log_chain, +) +from .client import ManagementClient +from .dpop import DPoPSigner, ES256DPoPSigner, generate_dpop_key +from .errors import ( + ApprovalDeniedError, + ApprovalError, + ApprovalExpiredError, + CboxIdApiError, + ManagementNetworkError, +) +from .generated import account as account_api +from .generated import environment as environment_api +from .generated import platform as platform_api +from .generated import workspace as workspace_api +from .generated.account import ACCOUNT_OPERATIONS, AccountClient +from .generated.environment import ENVIRONMENT_OPERATIONS, EnvironmentClient +from .generated.platform import PLATFORM_OPERATIONS, PlatformClient +from .generated.workspace import WORKSPACE_OPERATIONS, WorkspaceClient +from .models import ( + ApiResponse, + ApprovalContext, + ApprovalMode, + Danger, + HttpMethod, + OperationSpec, + Pagination, + PendingApproval, + PendingApprovalResult, + Plane, +) +from .transport import ManagementTransport, RetryOptions, retry_after_seconds + +__all__ = [ + "ACCOUNT_OPERATIONS", + "AUDIT_CHAIN_GENESIS", + "ENVIRONMENT_OPERATIONS", + "MAX_AUDIT_BATCH", + "PLATFORM_OPERATIONS", + "WORKSPACE_OPERATIONS", + "AccountClient", + "ApiResponse", + "ApprovalContext", + "ApprovalDeniedError", + "ApprovalError", + "ApprovalExpiredError", + "ApprovalMode", + "AuditChainVerification", + "AuditLogEventInput", + "AuditLogExportError", + "AuditLogRecord", + "AuditLogger", + "CboxIdApiError", + "CboxIdError", + "ConfigurationError", + "DPoPSigner", + "Danger", + "ES256DPoPSigner", + "EnvironmentClient", + "HttpMethod", + "ManagementClient", + "ManagementNetworkError", + "ManagementTransport", + "OperationSpec", + "Pagination", + "PendingApproval", + "PendingApprovalResult", + "Plane", + "PlatformClient", + "RetryOptions", + "WorkspaceClient", + "account_api", + "audit_event_document", + "audit_event_hash", + "canonical_json", + "environment_api", + "export_audit_logs", + "generate_dpop_key", + "platform_api", + "retry_after_seconds", + "verify_audit_chain", + "verify_audit_log_chain", + "workspace_api", +] diff --git a/src/cbox_id/management/audit_logs.py b/src/cbox_id/management/audit_logs.py new file mode 100644 index 0000000..19231dd --- /dev/null +++ b/src/cbox_id/management/audit_logs.py @@ -0,0 +1,523 @@ +"""Ergonomics for the environment plane's Audit Logs product. + +On top of the generated ``env.audit_logs.*`` methods: a buffered sender, an export that +waits until it is ready, and a client-side check of an organization's hash chain that is +byte-compatible with the server's. +""" + +from __future__ import annotations + +import hashlib +import math +import threading +import time +import uuid +from collections import deque +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import dataclass +from datetime import datetime, timezone +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Literal + +from typing_extensions import NotRequired, TypedDict + +from ..errors import CboxIdError, ConfigurationError +from .generated.environment import ( + AuditLogExport, + AuditLogsEventsCreateBodyEventsItem, + AuditLogsEventsCreateBodyEventsItemActor, + AuditLogsEventsCreateBodyEventsItemContext, + AuditLogsEventsCreateBodyEventsItemTargetsItem, + AuditLogsEventsListQuery, + AuditLogsExportsCreateBody, +) + +if TYPE_CHECKING: + from .generated.environment import EnvironmentClient + +#: One event as ``audit_logs.events.create`` takes it. +AuditLogEventInput = AuditLogsEventsCreateBodyEventsItem + +#: The most events one ``POST /audit-logs/events`` takes. +MAX_AUDIT_BATCH = 100 + +#: The ``prev_hash`` of an organization's first event: 64 zeros. +AUDIT_CHAIN_GENESIS = "0" * 64 + + +class AuditLogRecord(TypedDict): + """An event for :meth:`AuditLogger.record`: ``occurred_at`` defaults to now.""" + + #: The organization (your customer) the event happened in. + organization_id: str + #: What happened, as dotted words: ``invoice.voided``, ``user.signed_in``. + action: str + actor: AuditLogsEventsCreateBodyEventsItemActor + #: ISO 8601 with a time zone, to the millisecond. Defaults to now. + occurred_at: NotRequired[str] + targets: NotRequired[list[AuditLogsEventsCreateBodyEventsItemTargetsItem]] + context: NotRequired[AuditLogsEventsCreateBodyEventsItemContext] + metadata: NotRequired[dict[str, Any]] + + +def _now() -> str: + """Now, the way JavaScript's ``toISOString`` writes it: ``2026-10-08T09:15:30.123Z``.""" + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +# ── Buffered sender ───────────────────────────────────────────────────────────────────── + + +@dataclass(frozen=True, slots=True) +class _Batch: + events: list[AuditLogEventInput] + #: Fixed when the batch is cut, so every resend of it is the same request to the server. + idempotency_key: str + + +class AuditLogger: + """Buffers audit events and sends them in batches of up to 100. + + Each batch goes under its own ``Idempotency-Key``: on a full batch, every + ``flush_interval`` seconds, and on :meth:`flush` / :meth:`close`. + + Batches go out one at a time and in order, because the server appends each to its + organization's hash chain in the order received. A batch that fails stays queued with + the SAME key, so sending it again can never record an event twice. + + With ``flush_interval > 0`` a daemon thread sends in the background, and errors go to + ``on_error`` (the batch stays queued). With ``flush_interval=0`` there is no thread: a + full batch is sent from :meth:`record` itself, and everything else on :meth:`flush`. + Use it as a context manager, or call :meth:`close` on shutdown — a daemon thread does + not flush on its own when the process exits. + """ + + def __init__( + self, + client: EnvironmentClient, + *, + batch_size: int = MAX_AUDIT_BATCH, + flush_interval: float = 5.0, + on_error: Callable[[Exception, Sequence[AuditLogEventInput]], None] | None = None, + ) -> None: + if ( + not isinstance(batch_size, int) + or isinstance(batch_size, bool) + or not 1 <= batch_size <= MAX_AUDIT_BATCH + ): + raise ConfigurationError(f"batch_size must be 1–{MAX_AUDIT_BATCH}.") + + self._client = client + self._batch_size = batch_size + self._on_error = on_error + self._buffer: list[AuditLogEventInput] = [] + self._queue: deque[_Batch] = deque() + #: Guards the buffer, the queue and ``_closed``. + self._lock = threading.Lock() + #: One drain at a time, so batches reach the server in order. + self._sending = threading.Lock() + self._closed = False + self._stop = threading.Event() + self._wake = threading.Event() + self._interval = max(0.0, flush_interval) + self._thread: threading.Thread | None = None + + if self._interval > 0: + self._thread = threading.Thread( + target=self._run, name="cbox-id-audit-logger", daemon=True + ) + self._thread.start() + + @property + def pending(self) -> int: + """Events buffered or queued, not yet acknowledged by the server.""" + with self._lock: + return len(self._buffer) + sum(len(batch.events) for batch in self._queue) + + def record(self, event: AuditLogRecord) -> None: + """Buffer one event. A full batch is sent (in the background, when there is a thread).""" + with self._lock: + if self._closed: + raise CboxIdError("This AuditLogger is closed.") + + stamped: AuditLogEventInput = { + **event, + "occurred_at": event.get("occurred_at") or _now(), + } + self._buffer.append(stamped) + full = len(self._buffer) >= self._batch_size + + if full: + if self._thread is not None: + self._wake.set() + else: + self._background() + + def flush(self) -> None: + """Send everything buffered, and return once the server has it. Raises if a batch fails.""" + with self._sending: + self._cut() + self._drain() + + def close(self) -> None: + """Stop the background thread and flush.""" + with self._lock: + self._closed = True + + if self._thread is not None: + self._stop.set() + self._wake.set() + self._thread.join() + self._thread = None + + self.flush() + + def __enter__(self) -> AuditLogger: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def _run(self) -> None: + while not self._stop.is_set(): + self._wake.wait(self._interval) + self._wake.clear() + + if self._stop.is_set(): + return + + self._background() + + def _background(self) -> None: + try: + self.flush() + except Exception as exc: # noqa: BLE001 - reported, and the batch stays queued + if self._on_error is not None: + with self._lock: + head = list(self._queue[0].events) if self._queue else [] + self._on_error(exc, head) + + def _cut(self) -> None: + """Move the buffer into batches, each with the key every resend of it will carry.""" + with self._lock: + while self._buffer: + events = self._buffer[: self._batch_size] + del self._buffer[: self._batch_size] + self._queue.append(_Batch(events=events, idempotency_key=str(uuid.uuid4()))) + + def _drain(self) -> None: + while True: + with self._lock: + if not self._queue: + return + batch = self._queue[0] + + self._client.audit_logs.events.create( + {"events": batch.events}, idempotency_key=batch.idempotency_key + ) + + with self._lock: + self._queue.popleft() + + +# ── Exports ───────────────────────────────────────────────────────────────────────────── + + +class AuditLogExportError(CboxIdError): + """An export ended ``failed`` or ``expired``, or was not ready in time.""" + + def __init__(self, message: str, export: AuditLogExport | None) -> None: + super().__init__(message) + self.export = export + + +def export_audit_logs( + client: EnvironmentClient, + filters: AuditLogsExportsCreateBody | None = None, + *, + poll_interval: float = 2.0, + timeout: float = 600.0, + idempotency_key: str | None = None, +) -> AuditLogExport: + """Start a CSV export (``audit_logs.exports.create``) and read it until it is ``ready``. + + The result's ``url`` is signed and short-lived: download it straight away, or call + ``env.audit_logs.exports.get(id)`` again for a fresh one. Pass the create call's + ``idempotency_key`` to resume an export you started before. + """ + created = client.audit_logs.exports.create(filters or {}, idempotency_key=idempotency_key) + deadline = time.monotonic() + timeout + interval = max(0.0, poll_interval) + current = created.data + + while True: + state = current["state"] + + if state == "ready": + return current + + if state in ("failed", "expired"): + raise AuditLogExportError(f"Audit log export {current['id']} is {state}.", current) + + if time.monotonic() >= deadline: + raise AuditLogExportError( + f"Audit log export {current['id']} was not ready in time.", current + ) + + time.sleep(interval) + current = client.audit_logs.exports.get(current["id"]).data + + +# ── Chain verification ────────────────────────────────────────────────────────────────── + + +def _php_float(value: float) -> str: + """A float the way PHP's ``json_encode`` writes it (``serialize_precision = -1``).""" + if not math.isfinite(value): + raise ValueError("Canonical JSON cannot encode NaN or Infinity.") + + if value == 0: + return "-0" if math.copysign(1.0, value) < 0 else "0" + + sign = "-" if value < 0 else "" + # The shortest round-trip digits — the same digits PHP's mode-0 conversion picks. + shortest = Decimal(repr(abs(value))).normalize().as_tuple() + digits = "".join(str(d) for d in shortest.digits) + exponent = int(shortest.exponent) + decpt = len(digits) + exponent + + if decpt < -3 or decpt > 17: + scientific = decpt - 1 + mark = "-" if scientific < 0 else "+" + return f"{sign}{digits[0]}.{digits[1:] or '0'}e{mark}{abs(scientific)}" + + if decpt <= 0: + return f"{sign}0.{'0' * -decpt}{digits}" + + if decpt >= len(digits): + return f"{sign}{digits}{'0' * (decpt - len(digits))}" + + return f"{sign}{digits[:decpt]}.{digits[decpt:]}" + + +_ESCAPES = { + '"': '\\"', + "\\": "\\\\", + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", +} + + +def _php_string(value: str) -> str: + out = ['"'] + + for char in value: + code = ord(char) + + if char in _ESCAPES: + out.append(_ESCAPES[char]) + elif code < 0x20 or code in (0x2028, 0x2029): + # Control characters, and — like PHP even with JSON_UNESCAPED_UNICODE — the two + # line terminators JavaScript cannot take raw in a string. + out.append(f"\\u{code:04x}") + else: + out.append(char) + + out.append('"') + return "".join(out) + + +def canonical_json(value: object) -> str: + """Canonical JSON exactly as Cbox ID hashes it (``Cbox\\AuditChain\\Codec\\CanonicalJson``). + + Object keys sorted by their UTF-8 bytes at every depth, lists in order, slashes and + Unicode written as-is — plus the two places PHP's arrays show through, so the bytes + match: + + - an object whose sorted keys are ``"0"…"n-1"`` is a PHP list, and is written as an array; + - an empty object is an empty PHP array, written ``[]``. + + And, like PHP, U+2028 and U+2029 are escaped. Floats are written in PHP's form (``1.0`` + as ``1``, ``1e25`` as ``1.0e+25``). + """ + if value is None: + return "null" + if value is True: + return "true" + if value is False: + return "false" + if isinstance(value, int): + return str(value) + if isinstance(value, float): + return _php_float(value) + if isinstance(value, str): + return _php_string(value) + if isinstance(value, (list, tuple)): + return "[" + ",".join(canonical_json(item) for item in value) + "]" + if isinstance(value, Mapping): + entries = sorted( + ((str(key), item) for key, item in value.items()), + key=lambda entry: entry[0].encode("utf-8"), + ) + + if not entries: + return "[]" + + if all(key == str(index) for index, (key, _) in enumerate(entries)): + return "[" + ",".join(canonical_json(item) for _, item in entries) + "]" + + body = ",".join(f"{_php_string(key)}:{canonical_json(item)}" for key, item in entries) + return "{" + body + "}" + + raise TypeError(f"Canonical JSON cannot encode a {type(value).__name__}.") + + +def _metadata_of(value: object) -> object: + """Metadata as it is hashed: the value, or ``None`` for none — never an empty ``[]``/``{}``.""" + if isinstance(value, (list, tuple, Mapping)): + return value if len(value) > 0 else None + + return None + + +def _mapping(value: object) -> Mapping[str, Any]: + return value if isinstance(value, Mapping) else {} + + +def audit_event_document(event: Mapping[str, Any]) -> dict[str, Any]: + """The document an event's hash covers, in the server's shape. + + The server's ``AuditLogEventResource::document``. + """ + actor = _mapping(event.get("actor")) + context = _mapping(event.get("context")) + targets = event.get("targets") or [] + + return { + "id": event.get("id"), + "organization_id": event.get("organization_id"), + "sequence": event.get("sequence"), + "action": event.get("action"), + "occurred_at": event.get("occurred_at"), + "actor": { + "id": actor.get("id"), + "type": actor.get("type"), + "name": actor.get("name"), + "metadata": _metadata_of(actor.get("metadata")), + }, + "targets": [ + { + "id": target.get("id"), + "type": target.get("type"), + "name": target.get("name"), + "metadata": _metadata_of(target.get("metadata")), + } + for target in (_mapping(t) for t in targets) + ], + "context": { + "location": context.get("location"), + "user_agent": context.get("user_agent"), + }, + "metadata": _metadata_of(event.get("metadata")), + } + + +def audit_event_hash(previous_hash: str, event: Mapping[str, Any]) -> str: + """``sha256(previous_hash ‖ canonical_json(document))``, lowercase hex.""" + document = canonical_json(audit_event_document(event)) + return hashlib.sha256((previous_hash + document).encode("utf-8")).hexdigest() + + +ChainBreak = Literal["missing", "link", "hash"] + + +@dataclass(frozen=True, slots=True) +class AuditChainVerification: + """What :func:`verify_audit_chain` found — the server's ``AuditLogVerification``, minus + its view of the chain's head.""" + + valid: bool + verified_count: int + first_sequence: int | None + last_sequence: int | None + broken_at_sequence: int | None + #: ``missing`` (a sequence gap), ``link`` (``prev_hash`` does not name the event + #: before), ``hash`` (the event changed). + reason: ChainBreak | None + + +def verify_audit_chain( + events: Iterable[Mapping[str, Any]], *, previous_hash: str | None = None +) -> AuditChainVerification: + """Check one organization's events the way ``GET /audit-logs/verify`` does. + + In sequence order, every sequence present, every ``prev_hash`` naming the event before, + every ``hash`` matching the event. Takes events as the API returns them, in any order + (they are sorted by ``sequence``). Unlike the server's check it cannot see the chain's + head, so it cannot tell whether events were removed from the end. + + :param previous_hash: The hash the first event must chain from. Default: 64 zeros when + the first event is sequence 1; otherwise the first event's own ``prev_hash``, taken + on trust — pass the hash you kept for the event before it to check that link too. + """ + ordered = sorted(events, key=lambda event: int(event["sequence"])) + first = ordered[0] if ordered else None + + if previous_hash is not None: + previous = previous_hash + elif first is None or first["sequence"] == 1: + previous = AUDIT_CHAIN_GENESIS + else: + previous = str(first["prev_hash"]) + + expected = int(first["sequence"]) if first is not None else 1 + verified = 0 + + def result(reason: ChainBreak | None) -> AuditChainVerification: + return AuditChainVerification( + valid=reason is None, + verified_count=verified, + first_sequence=None if verified == 0 or first is None else int(first["sequence"]), + last_sequence=None if verified == 0 else expected - 1, + broken_at_sequence=None if reason is None else expected, + reason=reason, + ) + + for event in ordered: + if event["sequence"] != expected: + return result("missing") + if event["prev_hash"] != previous: + return result("link") + if audit_event_hash(previous, event) != event["hash"]: + return result("hash") + + previous = str(event["hash"]) + expected += 1 + verified += 1 + + return result(None) + + +def verify_audit_log_chain( + client: EnvironmentClient, + organization_id: str, + *, + limit: int | None = None, + previous_hash: str | None = None, +) -> AuditChainVerification: + """Read every event of one organization and verify its chain on this side. + + Independent of the server's own check (``env.audit_logs.verify()``). The events are held + in memory to be put in sequence order — the list is ordered by ``occurred_at``, which + the sender chose, not by ``sequence`` — so this suits a chain of thousands, not + millions. For a long chain, use the server's check, which pages by sequence. + """ + query: AuditLogsEventsListQuery = {"organization_id": organization_id, "order": "asc"} + + if limit is not None: + query["limit"] = limit + + return verify_audit_chain(client.audit_logs.events.list_all(query), previous_hash=previous_hash) diff --git a/src/cbox_id/management/client.py b/src/cbox_id/management/client.py new file mode 100644 index 0000000..bf70514 --- /dev/null +++ b/src/cbox_id/management/client.py @@ -0,0 +1,161 @@ +"""The base every generated management client extends.""" + +from __future__ import annotations + +from collections.abc import Mapping +from types import TracebackType +from typing import Any, ClassVar, Literal, overload + +import httpx +from typing_extensions import Self + +from .dpop import DPoPSigner +from .models import ApiResponse, ApprovalMode, HttpMethod, PendingApprovalResult, Plane +from .transport import AccessToken, ApprovalHandler, ManagementTransport, RetryOptions + + +class ManagementClient: + """Options and lifecycle shared by the four plane clients. + + Exactly one of ``api_key`` and ``access_token``. Server-side code only: every client + holds a management credential. + + :param base_url: Where the plane is served: an environment's own host + (``https://acme.cboxid.com``) for the environment and account planes, the + platform-root host for the workspace and platform planes. ``/api/v1`` is appended + unless it is already there. + :param api_key: A management key: ``cbid_env_…`` (environment plane) or ``cbid_ws_…`` + (workspace plane). + :param access_token: A delegated OAuth access token, or a callable returning one (called + before every request, so it can refresh). Platform and account planes accept nothing + else. + :param environment: Environment plane only, with a person's access token on the + PLATFORM ROOT's host: the environment to act in, by id or slug, sent as + ``Cbox-Environment`` on every request. + :param dpop: Present ``access_token`` as a DPoP-bound token, with a proof per request. + :param on_approval_required: Called when a write is held for a person's approval, before + the client starts polling — show ``approval.binding_code`` so they can match it on + their device. + :param approval_poll_interval: Seconds between approval polls when the server sends no + ``Retry-After``. + :param retry: Retry behaviour (see :class:`RetryOptions`). + :param timeout: Per-attempt timeout, in seconds. + :param headers: Headers sent on every request. Cannot override ``Authorization``. + :param transport: An ``httpx`` transport (e.g. ``httpx.MockTransport`` in tests). + :param http_client: An ``httpx.Client`` of your own (proxy, custom TLS). Not closed by + :meth:`close`. + """ + + _plane: ClassVar[Plane] + _default_base_url: ClassVar[str | None] = None + + #: The transport every method runs on — authentication, retries, approvals. + core: ManagementTransport + + def __init__( + self, + *, + base_url: str | None = None, + api_key: str | None = None, + access_token: AccessToken | None = None, + environment: str | None = None, + dpop: DPoPSigner | None = None, + on_approval_required: ApprovalHandler | None = None, + approval_poll_interval: float = 2.0, + retry: RetryOptions | None = None, + timeout: float = 30.0, + headers: Mapping[str, str] | None = None, + transport: httpx.BaseTransport | None = None, + http_client: httpx.Client | None = None, + ) -> None: + self.core = ManagementTransport( + self._plane, + base_url=base_url or self._default_base_url, + api_key=api_key, + access_token=access_token, + environment=environment, + dpop=dpop, + on_approval_required=on_approval_required, + approval_poll_interval=approval_poll_interval, + retry=retry, + timeout=timeout, + headers=headers, + transport=transport, + http_client=http_client, + ) + self._bind(self.core) + + def _bind(self, core: ManagementTransport) -> None: + """Attach the generated namespaces. Overridden by every generated client.""" + + @property + def base_url(self) -> str: + """The plane's base URL, ending in ``/api/v1``.""" + return self.core.base_url + + @overload + def request( + self, + method: HttpMethod, + path: str, + *, + query: Mapping[str, Any] | None = None, + body: Any = None, + approval: Literal["wait"] = "wait", + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any]: ... + + @overload + def request( + self, + method: HttpMethod, + path: str, + *, + query: Mapping[str, Any] | None = None, + body: Any = None, + approval: ApprovalMode, + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any] | PendingApprovalResult[Any]: ... + + def request( + self, + method: HttpMethod, + path: str, + *, + query: Mapping[str, Any] | None = None, + body: Any = None, + approval: ApprovalMode = "wait", + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any] | PendingApprovalResult[Any]: + """Call a route by method and path (relative to ``/api/v1``): anything not generated.""" + return self.core.request( + method, + path, + query=query, + body=body, + approval=approval, + idempotency_key=idempotency_key, + approval_id=approval_id, + headers=headers, + ) + + def close(self) -> None: + """Close the HTTP client, when the client created it.""" + self.core.close() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close() diff --git a/src/cbox_id/management/dpop.py b/src/cbox_id/management/dpop.py new file mode 100644 index 0000000..3f9d9e7 --- /dev/null +++ b/src/cbox_id/management/dpop.py @@ -0,0 +1,95 @@ +"""DPoP (RFC 9449) proofs for a sender-constrained access token. + +A DPoP-bound token (its ``cnf.jkt`` names a key) is presented as ``Authorization: DPoP +`` with a fresh proof per request, signed by that key: ``htm`` and ``htu`` pin the +request, ``ath`` pins the token, ``jti`` and ``iat`` make it single-use. A stolen token +without the private key is useless. +""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import time +import uuid +from typing import Protocol, runtime_checkable +from urllib.parse import urlsplit, urlunsplit + +import jwt +from cryptography.hazmat.primitives.asymmetric import ec + +from ..errors import ConfigurationError + + +@runtime_checkable +class DPoPSigner(Protocol): + """Produces the ``DPoP`` header value for one request.""" + + @property + def jkt(self) -> str: + """The RFC 7638 thumbprint of the public key — the ``jkt`` a bound token's ``cnf`` names.""" + ... + + def proof( + self, *, method: str, url: str, access_token: str, nonce: str | None = None + ) -> str: ... + + +def _b64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def _sha256(text: str) -> str: + return _b64url(hashlib.sha256(text.encode("utf-8")).digest()) + + +def generate_dpop_key() -> ec.EllipticCurvePrivateKey: + """A fresh P-256 private key for :class:`ES256DPoPSigner`.""" + return ec.generate_private_key(ec.SECP256R1()) + + +class ES256DPoPSigner: + """Signs ES256 proofs with the key the access token was bound to when it was issued + (the same one the token request's own DPoP proof used).""" + + def __init__(self, private_key: ec.EllipticCurvePrivateKey) -> None: + if not isinstance(private_key.curve, ec.SECP256R1): + raise ConfigurationError("A DPoP key must be an ECDSA P-256 key.") + + numbers = private_key.public_key().public_numbers() + self._key = private_key + #: The public key as a JWK, in RFC 7638's member order. + self.jwk: dict[str, str] = { + "crv": "P-256", + "kty": "EC", + "x": _b64url(numbers.x.to_bytes(32, "big")), + "y": _b64url(numbers.y.to_bytes(32, "big")), + } + # RFC 7638: the required members, lexicographic order, no whitespace. + self._jkt = _sha256(json.dumps(self.jwk, separators=(",", ":"), sort_keys=True)) + + @property + def jkt(self) -> str: + return self._jkt + + def proof(self, *, method: str, url: str, access_token: str, nonce: str | None = None) -> str: + parts = urlsplit(url) + payload: dict[str, object] = { + "jti": str(uuid.uuid4()), + "htm": method.upper(), + # RFC 9449 §4.2: the URI without query and fragment. + "htu": urlunsplit((parts.scheme, parts.netloc, parts.path, "", "")), + "iat": int(time.time()), + "ath": _sha256(access_token), + } + + if nonce is not None: + payload["nonce"] = nonce + + return jwt.encode( + payload, + self._key, + algorithm="ES256", + headers={"typ": "dpop+jwt", "jwk": self.jwk}, + ) diff --git a/src/cbox_id/management/errors.py b/src/cbox_id/management/errors.py new file mode 100644 index 0000000..0ceef22 --- /dev/null +++ b/src/cbox_id/management/errors.py @@ -0,0 +1,106 @@ +"""Exceptions raised by the management clients.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Literal + +from ..errors import CboxIdError +from .models import PendingApproval + + +class CboxIdApiError(CboxIdError): + """The management API answered with an error. + + Every plane answers a failure with the same envelope — ``{error, message}``, plus a + field-keyed ``errors`` map on ``validation_failed`` — so one class covers them all. + Branch on ``error`` (the stable machine code), never on ``message`` (prose, reworded + freely). + + The REQUEST body is never part of the error: management writes carry secrets (a + password you set, a key's scopes, a webhook secret) and production apps log exceptions. + """ + + def __init__( + self, + *, + status: int, + error: str, + message: str, + errors: Mapping[str, Sequence[str]] | None = None, + request_id: str | None = None, + retry_after: int | None = None, + ) -> None: + super().__init__(message) + #: HTTP status. + self.status = status + #: The stable machine code, e.g. ``validation_failed``, ``slug_taken``, ``not_found``. + self.error = error + #: The server's human-readable explanation. + self.message = message + #: On ``validation_failed`` only: the offending request fields, each mapped to its + #: messages (``{"name": ["The name field is required."]}``). Empty otherwise. + self.errors: dict[str, list[str]] = {k: list(v) for k, v in (errors or {}).items()} + #: The id the server served the request under (the envelope's ``request_id``, else + #: the ``X-Request-Id`` header). Quote it when reporting a problem. + self.request_id = request_id + #: Seconds to wait, off ``Retry-After`` — set on a ``429``/``503`` the client gave up + #: retrying. + self.retry_after = retry_after + + @property + def is_validation_error(self) -> bool: + """Whether this is a ``422 validation_failed`` with field errors.""" + return self.error == "validation_failed" + + def __repr__(self) -> str: + return ( + f"CboxIdApiError(status={self.status}, error={self.error!r}, " + f"message={self.message!r}, request_id={self.request_id!r})" + ) + + +class ManagementNetworkError(CboxIdError): + """The request never got an answer: DNS, TLS, a reset connection, a timeout. + + Raised after every retry the client was allowed. A write may or may not have happened; + repeating it with the same ``idempotency_key`` (``exc.idempotency_key``) is safe and + tells you which. + """ + + def __init__(self, message: str, idempotency_key: str | None) -> None: + super().__init__(message) + #: The ``Idempotency-Key`` the write was sent with, to repeat it safely. + self.idempotency_key = idempotency_key + + +ApprovalFailure = Literal["denied", "expired", "consumed"] + + +class ApprovalError(CboxIdError): + """An action held for a person's approval did not get it. + + ``reason`` says how: ``denied`` (they said no), ``expired`` (nobody answered in time), + or ``consumed`` (the approval was already spent by another request). + """ + + def __init__(self, message: str, reason: ApprovalFailure, approval: PendingApproval) -> None: + super().__init__(message) + self.reason: ApprovalFailure = reason + self.approval = approval + + +class ApprovalDeniedError(ApprovalError): + """The person declined the action on their device. Do not retry it unprompted.""" + + def __init__(self, approval: PendingApproval) -> None: + super().__init__(f"Approval {approval.id} was denied.", "denied", approval) + + +class ApprovalExpiredError(ApprovalError): + """Nobody approved the action before the approval expired. Ask again with a new request.""" + + def __init__(self, approval: PendingApproval) -> None: + super().__init__( + f"Approval {approval.id} expired before it was approved.", "expired", approval + ) diff --git a/src/cbox_id/management/generated/__init__.py b/src/cbox_id/management/generated/__init__.py new file mode 100644 index 0000000..6abbd0b --- /dev/null +++ b/src/cbox_id/management/generated/__init__.py @@ -0,0 +1 @@ +"""Generated by scripts/generate_management.py from the vendored OpenAPI documents.""" diff --git a/src/cbox_id/management/generated/account.py b/src/cbox_id/management/generated/account.py new file mode 100644 index 0000000..e54ce49 --- /dev/null +++ b/src/cbox_id/management/generated/account.py @@ -0,0 +1,373 @@ +# GENERATED by scripts/generate_management.py from openapi/account.yaml — do not edit. +# Regenerate with `python -m scripts.generate_management`. +"""Cbox ID — My Account API: typed client, schemas and operation table.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Literal, overload + +from typing_extensions import NotRequired, TypedDict + +from ..client import ManagementClient +from ..models import ApiResponse, ApprovalMode, OperationSpec, PendingApprovalResult +from ..transport import ManagementTransport + +# ── Schemas (components.schemas) ────────────────────────────────────────────────────────────────── + + +class Profile(TypedDict): + id: str + name: str | None + email: str | None + + +class SessionsRevoked(TypedDict): + #: How many sessions were signed out. + revoked: int + + +class PersonalApiKey(TypedDict): + id: str + name: NotRequired[str | None] + organization_id: str + client_id: str + prefix: NotRequired[str] + permissions: list[str] + expires_at: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: The key's value — shown once, on creation; null on an idempotent replay. + token: NotRequired[str | None] + + +class Error(TypedDict): + #: Stable machine code + error: str + + #: Human-readable explanation + message: str + + #: The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem. + request_id: NotRequired[str] + + #: Present on `validation_failed` only — the offending fields, each mapped to its messages. + errors: NotRequired[dict[str, list[str]]] + + +# ── Operation inputs and responses ──────────────────────────────────────────────────────────────── + + +class ActionApprovalsGetData(TypedDict): + id: str + status: Literal["pending", "approved", "denied", "expired", "consumed"] + + +class ApiKeysCreateBody(TypedDict): + #: The app this key is for. + client_id: str + name: str + + #: What the key may do in the app — each one a permission you hold there. Empty: it only identifies you. + permissions: NotRequired[list[str]] + + #: When it stops working. Left out, it does not expire. + expires_at: NotRequired[str | None] + + +class ProfileUpdateBody(TypedDict): + #: Your display name. + name: str + + +# ── Operations ──────────────────────────────────────────────────────────────────────────────────── + + +#: Every operation of the Cbox ID — My Account API, keyed by action name. +ACCOUNT_OPERATIONS: Mapping[str, OperationSpec] = { + "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/me/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), + "account.api_keys.create": OperationSpec(action="account.api_keys.create", operation_id="account_api_keys_create", method="POST", path="/me/organizations/{organization_id}/api-keys", path_params=("organization_id",), scope="account:api_keys:write", danger="critical", approval=True, body=True, pagination=None), + "account.api_keys.revoke": OperationSpec(action="account.api_keys.revoke", operation_id="account_api_keys_revoke", method="DELETE", path="/me/organizations/{organization_id}/api-keys/{key_id}", path_params=("organization_id", "key_id"), scope="account:api_keys:write", danger="destructive", approval=True, body=False, pagination=None), + "account.applications.revoke": OperationSpec(action="account.applications.revoke", operation_id="account_applications_revoke", method="DELETE", path="/me/applications/{client_id}", path_params=("client_id",), scope="account:applications:write", danger="destructive", approval=True, body=False, pagination=None), + "account.devices.remove": OperationSpec(action="account.devices.remove", operation_id="account_devices_remove", method="DELETE", path="/me/devices/{device_id}", path_params=("device_id",), scope="account:devices:write", danger="destructive", approval=True, body=False, pagination=None), + "account.organizations.leave": OperationSpec(action="account.organizations.leave", operation_id="account_organizations_leave", method="POST", path="/me/organizations/{organization_id}/leave", path_params=("organization_id",), scope="account:organizations:write", danger="destructive", approval=True, body=False, pagination=None), + "account.passkeys.remove": OperationSpec(action="account.passkeys.remove", operation_id="account_passkeys_remove", method="DELETE", path="/me/passkeys/{passkey_id}", path_params=("passkey_id",), scope="account:sign_in:write", danger="critical", approval=True, body=False, pagination=None), + "account.profile.update": OperationSpec(action="account.profile.update", operation_id="account_profile_update", method="PATCH", path="/me/profile", path_params=(), scope="account:profile:write", danger="write", approval=True, body=True, pagination=None), + "account.sessions.revoke": OperationSpec(action="account.sessions.revoke", operation_id="account_sessions_revoke", method="DELETE", path="/me/sessions/{session_id}", path_params=("session_id",), scope="account:sessions:write", danger="destructive", approval=True, body=False, pagination=None), + "account.sessions.revoke_others": OperationSpec(action="account.sessions.revoke_others", operation_id="account_sessions_revoke_others", method="POST", path="/me/sessions/revoke-others", path_params=(), scope="account:sessions:write", danger="critical", approval=True, body=False, pagination=None), + "account.social.unlink": OperationSpec(action="account.social.unlink", operation_id="account_social_unlink", method="DELETE", path="/me/social/{provider}", path_params=("provider",), scope="account:sign_in:write", danger="critical", approval=True, body=False, pagination=None), +} + + +# ── Namespaces ──────────────────────────────────────────────────────────────────────────────────── + + +class ActionApprovalsMethods: + """``action_approvals.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + def get(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ActionApprovalsGetData]: + """Where an approval you asked for stands + + The `poll_url` of a `202 approval_required`: an approval this token raised, and only those — another token's id is a 404. `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + + ``GET /me/action-approvals/{id}`` + """ + return self._core.call(ACCOUNT_OPERATIONS["action_approvals.get"], (id,), None, headers=headers) + + +class ApiKeysMethods: + """``api_keys.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, organization_id: str, body: ApiKeysCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PersonalApiKey]: ... + + @overload + def create(self, organization_id: str, body: ApiKeysCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PersonalApiKey] | PendingApprovalResult[PersonalApiKey]: ... + + def create(self, organization_id: str, body: ApiKeysCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PersonalApiKey] | PendingApprovalResult[PersonalApiKey]: + """Create an API key of your own for one of this environment's apps, in an organization you belong to. The value is returned once, as `token`. + + Requires scope `account:api_keys:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical. + + ``POST /me/organizations/{organization_id}/api-keys`` · action ``account.api_keys.create`` + Scope ``account:api_keys:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.api_keys.create"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, organization_id: str, key_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, organization_id: str, key_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, organization_id: str, key_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke one of your own API keys. + + Requires scope `account:api_keys:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive. + + ``DELETE /me/organizations/{organization_id}/api-keys/{key_id}`` · action ``account.api_keys.revoke`` + Scope ``account:api_keys:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.api_keys.revoke"], (organization_id, key_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ApplicationsMethods: + """``applications.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def revoke(self, client_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, client_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, client_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Withdraw an application's access to your account; it must ask you again to act as you. + + Requires scope `account:applications:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive. + + ``DELETE /me/applications/{client_id}`` · action ``account.applications.revoke`` + Scope ``account:applications:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.applications.revoke"], (client_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class DevicesMethods: + """``devices.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def remove(self, device_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, device_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, device_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove one of your trusted devices; it is no longer asked to approve anything. + + Requires scope `account:devices:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive. + + ``DELETE /me/devices/{device_id}`` · action ``account.devices.remove`` + Scope ``account:devices:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.devices.remove"], (device_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class OrganizationsMethods: + """``organizations.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def leave(self, organization_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def leave(self, organization_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def leave(self, organization_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Leave one of your own organizations. The last owner cannot leave — transfer ownership first. + + Requires scope `account:organizations:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive. + + ``POST /me/organizations/{organization_id}/leave`` · action ``account.organizations.leave`` + Scope ``account:organizations:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.organizations.leave"], (organization_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class PasskeysMethods: + """``passkeys.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def remove(self, passkey_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, passkey_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, passkey_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove one of your passkeys. + + Requires scope `account:sign_in:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical. + + ``DELETE /me/passkeys/{passkey_id}`` · action ``account.passkeys.remove`` + Scope ``account:sign_in:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.passkeys.remove"], (passkey_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ProfileMethods: + """``profile.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def update(self, body: ProfileUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Profile]: ... + + @overload + def update(self, body: ProfileUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Profile] | PendingApprovalResult[Profile]: ... + + def update(self, body: ProfileUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Profile] | PendingApprovalResult[Profile]: + """Change the display name on your own account. + + Requires scope `account:profile:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: write. + + ``PATCH /me/profile`` · action ``account.profile.update`` + Scope ``account:profile:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.profile.update"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SessionsMethods: + """``sessions.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def revoke(self, session_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, session_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, session_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Sign out one of your own sessions. + + Requires scope `account:sessions:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive. + + ``DELETE /me/sessions/{session_id}`` · action ``account.sessions.revoke`` + Scope ``account:sessions:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.sessions.revoke"], (session_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke_others(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SessionsRevoked]: ... + + @overload + def revoke_others(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SessionsRevoked] | PendingApprovalResult[SessionsRevoked]: ... + + def revoke_others(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SessionsRevoked] | PendingApprovalResult[SessionsRevoked]: + """Sign out every one of your sessions except the one you are using. + + Requires scope `account:sessions:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical. + + ``POST /me/sessions/revoke-others`` · action ``account.sessions.revoke_others`` + Scope ``account:sessions:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.sessions.revoke_others"], (), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SocialMethods: + """``social.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def unlink(self, provider: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def unlink(self, provider: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def unlink(self, provider: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Disconnect a social account from yours — never the last way you can sign in. + + Requires scope `account:sign_in:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical. + + ``DELETE /me/social/{provider}`` · action ``account.social.unlink`` + Scope ``account:sign_in:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ACCOUNT_OPERATIONS["account.social.unlink"], (provider,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +# ── Client ──────────────────────────────────────────────────────────────────────────────────────── + + +class AccountClient(ManagementClient): + """Cbox ID — My Account API. + + **Your own account** — the person calling, and nobody else: your profile, where you are + signed in, which applications may act as you, your own API keys for the apps built on + this environment, your sign-in methods, your trusted devices and the organizations you + belong to. The same actions the + console's My account pages run, with the same rules, refusals and audit trail. + + ``base_url`` is required: the environment's own host. + """ + + _plane = "account" + + def _bind(self, core: ManagementTransport) -> None: + self.action_approvals = ActionApprovalsMethods(core) + self.api_keys = ApiKeysMethods(core) + self.applications = ApplicationsMethods(core) + self.devices = DevicesMethods(core) + self.organizations = OrganizationsMethods(core) + self.passkeys = PasskeysMethods(core) + self.profile = ProfileMethods(core) + self.sessions = SessionsMethods(core) + self.social = SocialMethods(core) diff --git a/src/cbox_id/management/generated/environment.py b/src/cbox_id/management/generated/environment.py new file mode 100644 index 0000000..0f7ff43 --- /dev/null +++ b/src/cbox_id/management/generated/environment.py @@ -0,0 +1,7176 @@ +# GENERATED by scripts/generate_management.py from openapi/environment.yaml — do not edit. +# Regenerate with `python -m scripts.generate_management`. +"""Cbox ID — Environment Management API: typed client, schemas and operation table.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from typing import Any, Literal, TypeAlias, overload + +from typing_extensions import NotRequired, TypedDict + +from ..client import ManagementClient +from ..models import ApiResponse, ApprovalMode, OperationSpec, PendingApprovalResult +from ..transport import ManagementTransport + +# ── Schemas (components.schemas) ────────────────────────────────────────────────────────────────── + + +class Device(TypedDict): + id: str + install_id: str + platform: Literal["ios", "android"] + name: str + + #: Operator/user INTENT, not delivery health. A device can be active and + #: currently failing, or retired and perfectly reachable. + status: Literal["active", "retired"] + app_version: NotRequired[str | None] + os_version: NotRequired[str | None] + model: NotRequired[str | None] + last_seen_at: NotRequired[str | None] + created_at: NotRequired[str | None] + + +class ApprovalScopesItem(TypedDict): + scope: str + + #: Human wording, identical to the console. + label: str + + +class Approval(TypedDict): + #: The INTERNAL request id. Never `auth_req_id`, which is the initiating + #: client's polling secret and is stored only as a SHA-256 hash — a client must + #: not be able to approve its own request. + id: str + client_id: str + client_name: str + + #: The transaction description the user compares against what the requesting app + #: showed them. This is the anti-phishing primitive, and the reason approvals are + #: fetched rather than pushed. + binding_message: NotRequired[str | None] + scopes: list[ApprovalScopesItem] + organization_id: NotRequired[str | None] + requested_at: NotRequired[str | None] + expires_at: NotRequired[str | None] + + +class ApprovalDecision(TypedDict): + id: str + status: Literal["approved", "denied"] + + +class VaultSecret(TypedDict): + id: str + name: str + provider: str + owner_type: NotRequired[Literal["organization", "user"] | None] + owner_id: NotRequired[str | None] + expires_at: NotRequired[str | None] + + #: Whether the secret has been revoked. + revoked: bool + + +class Organization(TypedDict): + id: str + name: str + slug: str + type: Literal["customer", "reseller"] + status: Literal["active", "suspended", "deleted"] + parent_id: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: Free-form text values kept on the organization; null when there are none. + metadata: NotRequired[dict[str, str] | None] + + +class OrganizationDomain(TypedDict): + id: str + organization_id: str + + #: An email domain the organization claims, like acme.com. + domain: str + verified: bool + verified_at: NotRequired[str | None] + + #: Whether everyone signing in with an address here is routed to the organization's SSO. + capture: bool + + #: Where to publish the DNS TXT record that proves the claim. + record_name: str + + #: What to publish there. + record_value: str + + +#: The built-in tiers this deployment hands out. `owner` is never assigned — ownership +#: moves with `transfer-ownership`. +AssignableMemberRole: TypeAlias = Literal["admin", "member"] + + +class Member(TypedDict): + #: The membership's own id. Address a member by `user_id`. + id: str + user_id: str + organization_id: str + role: Literal["owner", "admin", "developer", "member", "viewer"] + status: Literal["active", "invited", "suspended"] + email: NotRequired[str | None] + name: NotRequired[str | None] + joined_at: NotRequired[str | None] + + +class Invitation(TypedDict): + id: str + organization_id: str + email: str + role: Literal["owner", "admin", "developer", "member", "viewer"] + + #: Ids of the access roles granted when the invitation is accepted. + roles: list[str] + status: Literal["pending", "accepted", "revoked"] + + #: The app it came from. + client_id: NotRequired[str | None] + + #: Where the person is sent after accepting. + return_to: NotRequired[str | None] + invited_at: NotRequired[str | None] + expires_at: NotRequired[str | None] + + +class Role(TypedDict): + id: str + + #: The app's manifest key; null for a role made in the console. + key: NotRequired[str | None] + name: str + description: NotRequired[str | None] + + #: The app that declared it; null for an app-agnostic role. + client_id: NotRequired[str | None] + + #: Set only for a role one organization made for itself. + organization_id: NotRequired[str | None] + + #: `false` marks a STAFF role: an organization's own administrators can never grant + #: it, nor can an invitation carry it. This API can — see the role endpoints. + tenant_assignable: bool + permissions: list[str] + + +class RoleAssignment(TypedDict): + role_id: str + key: NotRequired[str | None] + name: str + client_id: NotRequired[str | None] + tenant_assignable: NotRequired[bool] + user_id: str + + #: The organization it is held in; null for an environment-wide (staff) grant. + organization_id: NotRequired[str | None] + source: Literal["manual", "pushed", "system"] + + +class App(TypedDict): + id: str + client_id: str + name: str + + #: The kind, read back from its grants; `advanced` when they match no preset. + type: Literal["web", "spa", "cli", "service", "agent", "advanced"] + client_type: Literal["confidential", "public"] + organization_id: NotRequired[str | None] + first_party: bool + grant_types: list[str] + redirect_uris: list[str] + post_logout_redirect_uris: NotRequired[list[str]] + scopes: list[str] + + #: Where it publishes its roles-and-permissions manifest. + manifest_url: NotRequired[str | None] + + #: Its access-token lifetime in seconds; null for the install's default. + access_token_ttl: NotRequired[int | None] + + #: Where it is sent a logout token (OIDC Back-Channel Logout); null when it is not told. + backchannel_logout_uri: NotRequired[str | None] + backchannel_logout_session_required: NotRequired[bool] + + #: The prefix of its customer API keys; null when it accepts none. + api_key_prefix: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: Only on the response that created (or copied) the app, and only for an app that authenticates with a secret. Never again — null on an idempotent replay. + client_secret: NotRequired[str | None] + + +class AppSecret(TypedDict): + id: str + + #: The characters the secret ends in; null for one older than per-secret storage. + hint: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: Set on a secret a rotation replaced — it stops working then. + expires_at: NotRequired[str | None] + last_used_at: NotRequired[str | None] + + #: Only on the rotation that minted it. Never again — null on an idempotent replay. + client_secret: NotRequired[str | None] + + #: On a rotation — when the secrets it replaced stop working. + previous_expire_at: NotRequired[str | None] + + +class AppBlueprint(TypedDict): + """An app's configuration without its identity or credentials — deterministic (lists + sorted), versioned, and refused rather than half-read when it carries a key this + server does not know. + """ + + kind: Literal["cbox-id.client-blueprint"] + version: Literal[1] + name: str + client_type: Literal["confidential", "public"] + token_endpoint_auth_method: NotRequired[str | None] + grant_types: NotRequired[list[str]] + redirect_uris: NotRequired[list[str]] + post_logout_redirect_uris: NotRequired[list[str]] + scopes: NotRequired[list[str]] + first_party: NotRequired[bool] + manifest_url: NotRequired[str | None] + access_token_ttl: NotRequired[int | None] + backchannel_logout_uri: NotRequired[str | None] + backchannel_logout_session_required: NotRequired[bool] + api_key_prefix: NotRequired[str | None] + + +class ApiScope(TypedDict): + key: str + description: NotRequired[str | None] + + #: Whether apps an organization owns may be granted it; false keeps it for the environment's own apps. + tenant_requestable: NotRequired[bool] + + +class Api(TypedDict): + id: str + + #: Absolute URI; the token's `aud`. + identifier: str + name: str + + #: null when the environment owns it. + organization_id: NotRequired[str | None] + + #: The app whose roles and permissions it enforces. + client_id: NotRequired[str | None] + scopes: list[ApiScope] + created_at: NotRequired[str | None] + + +class Webhook(TypedDict): + id: str + url: str + + #: null when the environment owns it: it receives EVERY organization's events. + organization_id: str | None + event_types: list[str] + + #: false while paused. + active: bool + consecutive_failures: NotRequired[int] + last_success_at: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: The signing secret, on the create and rotate answers only — shown once, never + #: retrievable again. `null` on an idempotent replay of that answer. + secret: NotRequired[str | None] + + +class InlineHook(TypedDict): + id: str + url: str + hook_point: Literal["token_minting", "post_login", "pre_registration", "post_registration", "pre_password_change", "post_password_change"] + + #: null when the environment owns it: it runs for EVERY organization. + organization_id: str | None + active: bool + created_at: NotRequired[str | None] + + #: The signing secret, on the create answer only — shown once. `null` on an idempotent replay. + secret: NotRequired[str | None] + + +class LogStream(TypedDict): + id: str + name: str + destination: Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json"] + endpoint_url: str + auth: Literal["none", "bearer", "splunk", "hmac"] + + #: null when the environment owns it: it carries EVERY organization's entries. + organization_id: str | None + enabled: bool + consecutive_failures: NotRequired[int] + last_success_at: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: A generated HMAC key, on the create answer only — shown once. `null` on an idempotent replay. + secret: NotRequired[str | None] + + +class DomainEvent(TypedDict): + #: A ULID; pass the last one you saw as `after`. + id: str + + #: e.g. `user.created` — the webhook event name. + type: str + organization_id: str | None + occurred_at: str | None + payload: dict[str, Any] + + +class AuditEntry(TypedDict): + #: A ULID; pass the last one you saw as `after`. + id: str + + #: e.g. `webhook.created`. + action: str + actor_type: Literal["user", "service", "system", "operator", "organization_member"] + + #: For `service`, the management key's or app's id. + actor_id: str | None + organization_id: str | None + target_type: str | None + target_id: str | None + context: dict[str, Any] + ip: NotRequired[str | None] + recorded_at: str | None + + +class ApiKey(TypedDict): + id: str + name: NotRequired[str | None] + + #: What a person recognises the key by. Never the key. + prefix: str + organization_id: str + + #: Its holder. + user_id: str + + #: The app whose API it is for. + client_id: str + permissions: list[str] + status: Literal["active", "expired", "revoked"] + revoked: bool + created_at: NotRequired[str | None] + expires_at: NotRequired[str | None] + last_used_at: NotRequired[str | None] + revoked_at: NotRequired[str | None] + + +class SupportSessionAct(TypedDict): + """The RFC 8693 actor claim every token of the session carries.""" + + sub: str + + +class SupportSession(TypedDict): + id: str + + #: The customer's user being acted as. + user_id: str + organization_id: str + client_id: str + actor_id: str + + #: The RFC 8693 actor claim every token of the session carries. + act: SupportSessionAct + reason: str + scopes: list[str] + expires_at: str + + #: The first authorization code, when `redirect_uri` and `code_challenge` were sent. Shown once. + code: NotRequired[str | None] + redirect_uri: NotRequired[str | None] + + +class ApiKeyVerificationRequest(TypedDict): + #: The API key your API was presented with. + key: str + + #: For `client_secret_post`. + client_id: NotRequired[str] + + #: For `client_secret_post`. + client_secret: NotRequired[str] + + #: For `private_key_jwt`. + client_assertion_type: NotRequired[str] + + #: For `private_key_jwt`. + client_assertion: NotRequired[str] + + +class ApiKeyVerification(TypedDict): + active: bool + key_id: NotRequired[str] + + #: The key's holder. + sub: NotRequired[str] + org: NotRequired[str] + org_role: NotRequired[Literal["owner", "admin", "developer", "member", "viewer"] | None] + permissions: NotRequired[list[str]] + client_id: NotRequired[str] + expires_at: NotRequired[str | None] + + +class OAuthClientError(TypedDict): + """RFC 6749 §5.2 — the OAuth error shape, not this API's `{error, message}` envelope.""" + + error: Literal["invalid_client"] + error_description: NotRequired[str] + + +class User(TypedDict): + id: NotRequired[str] + email: NotRequired[str] + name: NotRequired[str | None] + status: NotRequired[Literal["active", "disabled", "locked"]] + email_verified_at: NotRequired[str | None] + + +class UserSession(TypedDict): + id: str + user_agent: NotRequired[str | None] + ip: NotRequired[str | None] + last_active_at: NotRequired[str | None] + expires_at: str + + #: Somebody else opened it as this person. + impersonation: bool + + +class Permission(TypedDict): + id: str + + #: The `feature:action` key an app reads out of a token. + name: str + description: NotRequired[str | None] + + #: The app that declared it; null for one authored here. + client_id: NotRequired[str | None] + + #: Set for one organization's own; null for the shared tier. + organization_id: NotRequired[str | None] + + #: Whether organizations may compose it into their own roles. + tenant_assignable: bool + + #: Authored here rather than declared by an app. + manual: bool + + #: Its app stopped declaring it. + orphaned: bool + + +class SignInRules(TypedDict): + #: Minimum password length. + min_length: int + + #: Passwords found in known breaches are refused. + require_breach_check: bool + + #: Days before a password must be changed; null for no limit. + max_age_days: int | None + + #: Previous passwords that may not be reused. + reuse_history: int + mfa: Literal["off", "optional", "required"] + + #: `required` refuses every other way in. + sso: Literal["off", "preferred", "required"] + + #: Failed attempts before a lockout; null for none. + lockout_threshold: int | None + + +class SignInPolicy(TypedDict): + #: null for the environment baseline. + organization_id: str | None + + #: What is in force at this level: the baseline, or the organization's override tightened over it. + policy: SignInRules + baseline: SignInRules + + #: What the organization stored itself; null when it inherits (and always for the baseline). + override: SignInRules | None + + #: True for an organization with no override of its own. + inheriting: bool + + +class SocialProvider(TypedDict): + """Its client secret and private key are write-only and never returned.""" + + id: str + organization_id: str | None + + #: The catalogue key: google, github, apple… + provider: str | None + name: str + protocol: Literal["oidc", "oauth2"] + status: str + + #: The redirect URI to register with the provider. + callback_uri: str + created_at: NotRequired[str | None] + + +class FrontendKey(TypedDict): + """A publishable key. Returned in full: it is public by design; the origins are the control.""" + + id: str + name: str + key: str + mode: Literal["test", "live"] + origins: list[str] + active: bool + created_at: NotRequired[str | None] + last_used_at: NotRequired[str | None] + revoked_at: NotRequired[str | None] + + +class SamlAppAttributeMappingsItem(TypedDict): + attribute: str + field: str + + +class SamlApp(TypedDict): + """An application that trusts this environment as its SAML identity provider. Its certificate is write-only. An application owned by an organization is only ever asserted to that organization's active members; one with no organization is open to every person in the environment.""" + + id: str + entity_id: str + acs_url: str + name_id_format: str + name_id_attribute: str + attribute_mappings: list[SamlAppAttributeMappingsItem] + want_authn_requests_signed: bool + has_certificate: bool + + #: The organization whose active members alone may sign in to it; null for an environment-wide application. + organization_id: str | None + status: str + created_at: NotRequired[str | None] + updated_at: NotRequired[str | None] + + +class ErasureReceiptStepsItem(TypedDict): + #: The store, e.g. `identity.credentials`, `oauth.grants`, `devices.devices`. + step: str + + #: What was removed or rewritten there, by item. + counts: dict[str, int] + + #: Why a step could not act, when it could not. + note: str | None + + +class ErasureReceipt(TypedDict): + """The record of one erasure (GDPR Art. 17): what each store did, in numbers. Carries no personal data — safe to log, file with your Art. 30 records, or hand to the person who asked.""" + + + #: The erased person's id. Kept: the row is pseudonymised in place, not deleted. + subject_id: str + erased_at: str + + #: Whether the account row's email and name were replaced with placeholders. + subject_pseudonymised: bool + + #: The `user.erased` tombstone on the audit trail. + audit_entry_id: str | None + + #: One entry per store, in the order they ran — including stores that held nothing, so a receipt shows a store was checked. + steps: list[ErasureReceiptStepsItem] + + +class LegacyLogin(TypedDict): + """The legacy sign-in endpoint an app declared for migration. Its shared secret is never returned.""" + + declared: bool + url: str | None + + #: The app that declared it. + client_id: str | None + + #: That app's name. + declared_by: str | None + approved: bool + approved_at: str | None + + #: The person or key that approved it. + approved_by: str | None + + +class ThemeMode(TypedDict): + primary: NotRequired[str] + background: NotRequired[str] + foreground: NotRequired[str] + muted: NotRequired[str] + + +class AppearanceTheme(TypedDict): + preset: NotRequired[str] + radius: NotRequired[str] + font: NotRequired[str] + light: NotRequired[ThemeMode] + dark: NotRequired[ThemeMode] + + +class Appearance(TypedDict): + #: null for the environment default. + organization_id: str | None + + #: Whether this level has a theme of its own. + customized: bool + theme: AppearanceTheme + logo: str | None + + +class WhitelabelBranding(TypedDict): + #: null for the environment default. + organization_id: str | None + palette: dict[str, str | None] + app_name: str | None + email_from_name: str | None + email_template: str | None + + #: Uploaded from the console. + logo_url: str | None + + #: Uploaded from the console. + favicon_url: str | None + + +class CustomDomainPendingOption1(TypedDict): + domain: str + record_name: str + record_value: str + + +class CustomDomain(TypedDict): + #: The verified domain serving this environment. + domain: str | None + verified_at: str | None + + #: A domain waiting for its DNS proof, and the TXT record to publish. + pending: CustomDomainPendingOption1 | None + + +class SsoConnectionConfig(TypedDict): + """The settings that are not secrets.""" + + idp_entity_id: NotRequired[str] + idp_sso_url: NotRequired[str] + sp_entity_id: NotRequired[str] + sp_acs_url: NotRequired[str] + issuer: NotRequired[str] + client_id: NotRequired[str] + + +class SsoConnectionServiceProvider(TypedDict): + """What to paste into the identity provider: this connection's own entity id and ACS URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection.""" + + sp_entity_id: NotRequired[str] + sp_acs_url: NotRequired[str] + redirect_uri: NotRequired[str] + + +class SsoConnection(TypedDict): + """A SAML or OIDC connection an organization signs in through. Its certificate, client secret and signing key are write-only.""" + + id: str + + #: null when the environment owns it: it signs people in and enrols them nowhere. + organization_id: str | None + name: str + type: Literal["saml", "oidc", "oauth2"] + + #: The catalogue provider a social sign-in connection came from; null for an organization's own identity provider. + provider: NotRequired[str | None] + status: Literal["draft", "active", "inactive"] + active: bool + + #: false for a draft created with pending_idp whose identity-provider details are still missing. Activation refuses it. + complete: NotRequired[bool] + + #: The settings that are not secrets. + config: SsoConnectionConfig + + #: What to paste into the identity provider: this connection's own entity id and ACS URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection. + service_provider: NotRequired[SsoConnectionServiceProvider | None] + created_at: NotRequired[str | None] + updated_at: NotRequired[str | None] + + +class SsoCertificatesCertificatesItem(TypedDict): + #: primary signs today; staged is trusted beside it until activated. + role: Literal["primary", "staged"] + + #: false for a certificate on file that could not be parsed — still trusted. + readable: bool + fingerprint_sha256: str | None + subject: str | None + issuer: str | None + not_before: str | None + not_after: str | None + days_remaining: int | None + expired: bool | None + + +class SsoCertificatesChecksItem(TypedDict): + check: Literal["readable", "not_expired", "key_strength", "not_on_file", "valid_now"] + passed: bool + + +class SsoCertificates(TypedDict): + """A SAML connection's signing certificates. The certificates themselves are never returned.""" + + connection_id: str + organization_id: str | None + + #: When the connection stops working if nothing is done: the latest expiry among the certificates it trusts. + expires_at: str | None + days_remaining: int | None + certificates: list[SsoCertificatesCertificatesItem] + + #: On the stage answer only: what the new certificate was tested for. + checks: NotRequired[list[SsoCertificatesChecksItem]] + + +class LogStreamTest(TypedDict): + id: str + + #: Whether the destination accepted the test entry. + delivered: bool + + #: The destination's refusal, scrubbed of the stream's secret. + error: str | None + tested_at: str + + +class SsoDomainVerificationOption1(TypedDict): + type: Literal["TXT"] + name: str + value: str + + +class SsoDomain(TypedDict): + id: str + organization_id: str + domain: str + verified: bool + verified_at: NotRequired[str | None] + + #: Everyone with an address at the domain must sign in through the organization's SSO. + capture: bool + + #: The DNS TXT record that proves the domain, while it is unverified. + verification: SsoDomainVerificationOption1 | None + + +class SamlMetadata(TypedDict): + """An identity provider's settings read from its SAML metadata. Nothing is stored.""" + + idp_entity_id: str + idp_sso_url: str + + #: The IdP's public signing certificate, from its own metadata. + idp_x509cert: str + + +class PortalLink(TypedDict): + id: str + organization_id: str + + #: What the link may set up. + intents: list[Literal["sso", "dsync", "domain_verification", "log_streams", "certificate_renewal", "audit_logs"]] + + #: The address the link was mailed to, or null when it was not sent. + emailed_to: str | None + + #: The one-time setup link — the whole credential. Shown once; `null` on an + #: idempotent replay of this answer. + url: str | None + expires_at: str | None + + +class AuditLogEventActor(TypedDict): + id: str + type: str + name: str | None + metadata: dict[str, Any] | None + + +class AuditLogEventTargetsItem(TypedDict): + id: str + type: str + name: str | None + metadata: dict[str, Any] | None + + +class AuditLogEventContext(TypedDict): + location: str | None + user_agent: str | None + + +class AuditLogEvent(TypedDict): + """One audit event your app sent. `hash` covers `id`, `organization_id`, `sequence`, + `action`, `occurred_at`, `actor`, `targets`, `context` and `metadata`, chained to + the organization's previous event by `prev_hash` — see the Audit Logs guide for how + to verify it yourself. + """ + + id: str + organization_id: str + + #: The event's place in its organization's chain, from 1. + sequence: int + + #: e.g. `invoice.voided`. + action: str + + #: When it happened, as the sender said, to the millisecond. + occurred_at: str + actor: AuditLogEventActor + targets: list[AuditLogEventTargetsItem] + context: AuditLogEventContext + metadata: dict[str, Any] | None + + #: The version of its action's schema it was checked against; null when the action has none. + schema_version: int | None + + #: When it arrived. Retention counts from here. + received_at: str | None + + #: The previous event's hash; 64 zeros for an organization's first. + prev_hash: str + hash: str + + +class AuditLogEventBatch(TypedDict): + #: The recorded events, in the order they were sent. + events: list[AuditLogEvent] + + +class AuditLogSchemaTargetsItem(TypedDict): + type: str + metadata: dict[str, Any] | None + + +class AuditLogSchema(TypedDict): + id: str + action: str + + #: Starts at 1; every replacement is the next. + version: int + + #: The target types this action's events may name; null allows any. + targets: list[AuditLogSchemaTargetsItem] | None + + #: A metadata schema (a subset of JSON Schema). + actor_metadata: dict[str, Any] | None + + #: A metadata schema (a subset of JSON Schema). + metadata: dict[str, Any] | None + created_at: str | None + updated_at: str | None + + +class AuditLogExport(TypedDict): + id: str + + #: null for an export across the whole environment. + organization_id: str | None + state: Literal["pending", "ready", "failed", "expired"] + + #: The filters it was asked with. + filters: dict[str, Any] + row_count: int | None + + #: Where the CSV downloads from once `ready` — signed, and valid for a few minutes; + #: read the export again for a fresh one. null otherwise, and on an idempotent replay. + url: str | None + created_at: str | None + completed_at: str | None + + #: When the file is deleted. + expires_at: str | None + + +class AuditLogSettings(TypedDict): + #: Events are kept this many days after they arrive. + retention_days: int + + #: Whether an action with no schema is refused. + strict_schemas: bool + + +class AuditLogVerification(TypedDict): + organization_id: str + + #: Every event checked is unchanged and in place. + valid: bool + + #: The check reached the chain's head. When false, continue from last_sequence + 1. + complete: bool + verified_count: int + first_sequence: int | None + last_sequence: int | None + head_sequence: int + broken_at_sequence: int | None + reason: Literal["missing", "link", "hash", "truncated"] | None + + +class Directory(TypedDict): + """A directory syncing an organization's people in. Its token and credentials are never returned after they are minted.""" + + id: str + organization_id: str + name: str + provider: Literal["scim", "google_workspace", "microsoft_entra"] + + #: true when this platform fetches from the provider; false for a SCIM endpoint the provider posts to. + pull: bool + active: bool + status: Literal["active", "paused"] + + #: Where the identity provider sends SCIM requests. null for a pull directory. + scim_base_url: NotRequired[str | None] + last_synced_at: NotRequired[str | None] + last_sync_error: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: The SCIM bearer token, on the create and rotate answers only — shown once, never + #: retrievable again. `null` on an idempotent replay of that answer. + bearer_token: NotRequired[str | None] + + +class DirectoryGroup(TypedDict): + id: str + directory_id: str + name: str + external_id: NotRequired[str | None] + + #: The roles everyone in the group holds. + role_ids: list[str] + + +class ProvisioningTarget(TypedDict): + """A downstream app people are pushed to over SCIM. Its credential is write-only.""" + + id: str + + #: null when it receives every organization's people. + organization_id: str | None + name: str + base_url: str + auth_scheme: Literal["bearer", "oauth2_client_credentials"] + token_url: NotRequired[str | None] + client_id: NotRequired[str | None] + + #: false while paused. + active: bool + consecutive_failures: NotRequired[int] + last_success_at: NotRequired[str | None] + last_error: NotRequired[str | None] + created_at: NotRequired[str | None] + + +class SodPolicy(TypedDict): + """A role-conflict (segregation of duties) rule: roles no one person may hold together.""" + + id: str + + #: null when it binds every organization. + organization_id: str | None + name: str + description: NotRequired[str | None] + role_ids: list[str] + active: bool + created_at: NotRequired[str | None] + + +class AccessReview(TypedDict): + id: str + + #: null for a staff review of environment-wide grants. + organization_id: str | None + staff: bool + name: str + status: Literal["open", "closed"] + open: bool + + #: What closing does to an item nobody decided. + pending_policy: Literal["revoke", "certify"] + item_count: int + due_at: NotRequired[str | None] + closed_at: NotRequired[str | None] + + #: The person or key that opened it. + created_by: NotRequired[str | None] + created_at: NotRequired[str | None] + + +class AccessReviewItem(TypedDict): + id: str + review_id: str + subject_id: str + access_type: Literal["role", "membership", "environment_role"] + + #: The role id, or the organization id of a membership. + access_ref: str + organization_id: NotRequired[str | None] + decision: Literal["pending", "certified", "revoked"] + decided_by: NotRequired[str | None] + decided_at: NotRequired[str | None] + note: NotRequired[str | None] + + #: Whether a revoke was carried out when the review closed. + applied: bool + + #: Why a revoke could not be applied. + application_note: NotRequired[str | None] + + +class TokenVaultSecret(TypedDict): + """A downstream credential in the token vault. Its value is write-only.""" + + id: str + + #: null for the environment's own secrets. + organization_id: str | None + name: str + provider: str + status: Literal["active", "expired", "revoked"] + revoked: bool + expires_at: NotRequired[str | None] + rotated_at: NotRequired[str | None] + created_at: NotRequired[str | None] + + #: The OAuth client ids that may lease it. Not on lists. + grants: NotRequired[list[str]] + + +class AgentRequestSubject(TypedDict): + """Whom the agent asks to act as.""" + + id: str + + #: Null when that person no longer exists — the request can still be denied. + email: str | None + + +class AgentRequest(TypedDict): + """A pending request from an agent to act as one of this environment's people (OIDC CIBA).""" + + id: str + client_id: str + + #: The app's name, or its client id when it has none. + app: str + + #: Whom the agent asks to act as. + subject: AgentRequestSubject + + #: The message the person sees on their device, to recognise the request. + binding_message: NotRequired[str | None] + scopes: list[str] + expires_at: str | None + created_at: NotRequired[str | None] + + +class PageMeta(TypedDict): + limit: NotRequired[int] + has_more: NotRequired[bool] + next_cursor: NotRequired[str | None] + + +class Error(TypedDict): + #: Stable machine code + error: str + + #: Human-readable explanation + message: str + + #: The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem. + request_id: NotRequired[str] + + #: Present on `validation_failed` only: the offending request fields, each + #: mapped to its messages. + errors: NotRequired[dict[str, list[str]]] + + +class BearerChallenge(TypedDict): + """RFC 6750 §3 error shape, used by the OAuth-token-authenticated Token Vault + endpoints. NOT the `{error, message}` envelope — restating the envelope here + would be a protocol conformance bug. + """ + + error: Literal["invalid_request", "invalid_token", "insufficient_scope"] + error_description: str + + +# ── Operation inputs and responses ──────────────────────────────────────────────────────────────── + + +class AccessReviewsCloseBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class AccessReviewsCreateBody(TypedDict): + #: What the review is: "Q3 finance access". + name: str + + #: organization (default): one organization's access, named by organization_id. staff: every environment-wide staff grant. + covers: NotRequired[Literal["organization", "staff"]] + + #: The organization to review. Required unless covers is staff. + organization_id: NotRequired[str | None] + + #: Days until it is due. Default 7. + due_in_days: NotRequired[int] + + +class AccessReviewsGetQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class AccessReviewsItemsDecideBody(TypedDict): + #: certified keeps the access; revoked takes it away when the review closes. + decision: Literal["certified", "revoked"] + + #: Why, for whoever audits the review. + note: NotRequired[str | None] + + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class AccessReviewsItemsListQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +AccessReviewsItemsListData: TypeAlias = 'list[AccessReviewItem]' + + +class AccessReviewsListQuery(TypedDict): + #: Only reviews covering this organization: its own and the staff reviews. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +AccessReviewsListData: TypeAlias = 'list[AccessReview]' + + +class ActionApprovalsGetData(TypedDict): + id: str + status: Literal["pending", "approved", "denied", "expired", "consumed"] + + +class ApiKeysListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + client_id: NotRequired[str] + + +ApiKeysListData: TypeAlias = 'list[ApiKey]' + + +class ApisCreateBody(TypedDict): + identifier: str + name: str + organization_id: NotRequired[str | None] + client_id: NotRequired[str | None] + scopes: NotRequired[list[ApiScope]] + + +class ApisListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + + +ApisListData: TypeAlias = 'list[Api]' + + +class ApisScopesDefineBody(TypedDict): + description: NotRequired[str | None] + + #: Whether organizations' own apps may request it. Default true. + tenant_requestable: NotRequired[bool] + + +class ApisUpdateBody(TypedDict): + name: NotRequired[str] + client_id: NotRequired[str | None] + scopes: NotRequired[list[ApiScope]] + + +class ApprovalsListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +ApprovalsListData: TypeAlias = 'list[AgentRequest]' + + +class AppsCopyBody(TypedDict): + #: The environment to copy into: another environment of this project. + environment_id: str + + #: What to call the copy there. + name: str + + #: Its redirect URIs there — staging's callback is rarely production's. Left out, none. + redirect_uris: NotRequired[list[str]] + + +class AppsCreateBody(TypedDict): + blueprint: NotRequired[AppBlueprint] + name: NotRequired[str] + type: NotRequired[Literal["web", "spa", "cli", "service", "agent", "advanced"]] + client_type: NotRequired[Literal["confidential", "public"]] + grant_types: NotRequired[list[str]] + redirect_uris: NotRequired[list[str]] + post_logout_redirect_uris: NotRequired[list[str]] + scopes: NotRequired[list[str]] + first_party: NotRequired[bool] + + #: Where the app publishes its roles-and-permissions manifest (short form). + manifest_url: NotRequired[str | None] + + #: The owning organization; null for the environment's own app. + organization_id: NotRequired[str | None] + + #: A public JWK Set, for a `private_key_jwt` app. + jwks: NotRequired[dict[str, Any] | None] + + +class AppsListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + + +AppsListData: TypeAlias = 'list[App]' + + +class AppsManifestSetBody(TypedDict): + #: An absolute URL; null or left out clears it. + manifest_url: NotRequired[str | None] + + +AppsManifestSyncData: TypeAlias = dict[str, Any] + + +class AppsScopesSetBody(TypedDict): + #: Required. The COMPLETE scope set afterwards; an empty list removes every scope. + scopes: NotRequired[list[str]] + + +AppsSecretsListData: TypeAlias = 'list[AppSecret]' + + +class AppsSecretsRotateBody(TypedDict): + #: How long the current secrets keep working, in seconds. 0 stops them at once. + grace_seconds: int + + +class AppsSettingsApiKeyPrefixBody(TypedDict): + #: 2–16 lowercase letters or digits starting with a letter, then `_live` or `_test`. Null or left out: no new keys. + prefix: NotRequired[str | None] + + +class AppsSettingsBackchannelLogoutBody(TypedDict): + #: HTTPS (HTTP on localhost only). Null or left out: the app is not told. + uri: NotRequired[str | None] + + #: Every logout token carries `sid`. Default false. + session_required: NotRequired[bool] + + +class AppsSettingsTokenExchangeBody(TypedDict): + enabled: bool + + +class AppsSettingsTokenLifetimeBody(TypedDict): + #: Seconds, from 60 to the install's ceiling. Null or left out: the install's default. + access_token_ttl: NotRequired[int | None] + + +class AppsUpdateBody(TypedDict): + name: NotRequired[str] + + #: The COMPLETE list afterwards. + redirect_uris: NotRequired[list[str]] + + #: The COMPLETE list afterwards. + post_logout_redirect_uris: NotRequired[list[str]] + + +class AuditListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + #: Only this action, e.g. `webhook.created`. + action: NotRequired[str] + + #: Only entries by this kind of actor; `service` is a management key or an app. + actor_type: NotRequired[Literal["user", "service", "system", "operator", "organization_member"]] + + #: Only this organization's entries. + organization_id: NotRequired[str] + + +AuditListData: TypeAlias = 'list[AuditEntry]' + + +class AuditLogsEventsCreateBodyEventsItemActor(TypedDict): + #: Your id for whoever did it. + id: str + + #: What kind of actor: `user`, `api_key`, `system`. + type: str + + #: A name a person reads in the log. + name: NotRequired[str | None] + + #: Up to 50 names to strings, numbers, booleans or null. + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsEventsCreateBodyEventsItemTargetsItem(TypedDict): + id: str + + #: What kind of thing: `invoice`, `user`, `report`. + type: str + name: NotRequired[str | None] + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsEventsCreateBodyEventsItemContext(TypedDict): + #: Where it came from — usually the client's IP address. + location: NotRequired[str | None] + user_agent: NotRequired[str | None] + + +class AuditLogsEventsCreateBodyEventsItem(TypedDict): + #: The organization (your customer) the event happened in. + organization_id: str + + #: What happened, as dotted words: `invoice.voided`, `user.signed_in`. + action: str + + #: When it happened: ISO 8601 with a time zone, to the millisecond. + occurred_at: str + actor: AuditLogsEventsCreateBodyEventsItemActor + + #: What it was done to. + targets: NotRequired[list[AuditLogsEventsCreateBodyEventsItemTargetsItem]] + context: NotRequired[AuditLogsEventsCreateBodyEventsItemContext] + + #: Up to 50 names to strings, numbers, booleans or null; values up to 500 characters. + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsEventsCreateBody(TypedDict): + #: 1–100 events, recorded all together or not at all. + events: list[AuditLogsEventsCreateBodyEventsItem] + + +class AuditLogsEventsListQuery(TypedDict): + #: Only this organization's events. Required in effect for an organization's own administrator, whose events are the only ones they see. + organization_id: NotRequired[str] + + #: Only these actions, e.g. `invoice.voided`. + actions: NotRequired[list[str]] + + #: Only events this actor caused. + actor_id: NotRequired[str] + + #: Only events naming this target. + target_id: NotRequired[str] + + #: Only events that occurred at or after this time. + range_start: NotRequired[str] + + #: Only events that occurred before this time. + range_end: NotRequired[str] + + #: `desc` (default) for newest first, `asc` for oldest first. + order: NotRequired[Literal["desc", "asc"]] + + #: Events per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +AuditLogsEventsListData: TypeAlias = 'list[AuditLogEvent]' + + +class AuditLogsExportsCreateBody(TypedDict): + #: Only this organization's events. Required in effect for an organization's own administrator, whose events are the only ones they see. + organization_id: NotRequired[str] + + #: Only these actions, e.g. `invoice.voided`. + actions: NotRequired[list[str]] + + #: Only events this actor caused. + actor_id: NotRequired[str] + + #: Only events naming this target. + target_id: NotRequired[str] + + #: Only events that occurred at or after this time. + range_start: NotRequired[str] + + #: Only events that occurred before this time. + range_end: NotRequired[str] + + +class AuditLogsSchemasCreateBodyTargetsItem(TypedDict): + #: A target type events of this action may name. + type: str + + #: A metadata schema for targets of this type. + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsSchemasCreateBody(TypedDict): + #: The action this schema is for, e.g. `invoice.voided`. + action: str + + #: The target types this action's events may name, each with an optional metadata schema. Leave out to allow any. + targets: NotRequired[list[AuditLogsSchemasCreateBodyTargetsItem] | None] + + #: A metadata schema (a subset of JSON Schema) for the actor's metadata. + actor_metadata: NotRequired[dict[str, Any]] + + #: A metadata schema (a subset of JSON Schema) for the event's own metadata. + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsSchemasListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +AuditLogsSchemasListData: TypeAlias = 'list[AuditLogSchema]' + + +class AuditLogsSchemasUpdateBodyTargetsItem(TypedDict): + #: A target type events of this action may name. + type: str + + #: A metadata schema for targets of this type. + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsSchemasUpdateBody(TypedDict): + #: The target types this action's events may name, each with an optional metadata schema. Leave out to allow any. + targets: NotRequired[list[AuditLogsSchemasUpdateBodyTargetsItem] | None] + + #: A metadata schema (a subset of JSON Schema) for the actor's metadata. + actor_metadata: NotRequired[dict[str, Any]] + + #: A metadata schema (a subset of JSON Schema) for the event's own metadata. + metadata: NotRequired[dict[str, Any]] + + +class AuditLogsSettingsUpdateBody(TypedDict): + #: Keep events this many days after they arrive. + retention_days: NotRequired[int] + + #: Refuse events whose action has no schema. + strict_schemas: NotRequired[bool] + + +class AuditLogsVerifyQuery(TypedDict): + #: The organization whose chain to verify. An organization's own administrator may leave it out. + organization_id: NotRequired[str] + + #: Start here rather than at the oldest event kept. + from_sequence: NotRequired[int] + + #: Verify at most this many events. Default 10,000. + limit: NotRequired[int] + + +class BrandingAppearanceGetQuery(TypedDict): + #: Read this organization's theme. Left out, the environment default. + organization_id: NotRequired[str] + + +class BrandingAppearanceSetBodyThemeLight(TypedDict): + """Colours as #rrggbb. One left out takes the preset's.""" + + primary: NotRequired[str] + background: NotRequired[str] + foreground: NotRequired[str] + muted: NotRequired[str] + + +class BrandingAppearanceSetBodyThemeDark(TypedDict): + """Colours as #rrggbb. One left out takes the preset's.""" + + primary: NotRequired[str] + background: NotRequired[str] + foreground: NotRequired[str] + muted: NotRequired[str] + + +class BrandingAppearanceSetBodyTheme(TypedDict): + """The theme. Both modes must be readable: text needs 4.5:1 against the background, muted text 3:1.""" + + + #: A preset id: cbox, midnight, minimal, warm, contrast. + preset: NotRequired[str] + + #: Corner radius: 0rem, 0.25rem, 0.375rem, 0.5rem, 0.75rem, 1rem. + radius: NotRequired[str] + + #: Type family: system, geometric, serif. + font: NotRequired[str] + + #: Colours as #rrggbb. One left out takes the preset's. + light: NotRequired[BrandingAppearanceSetBodyThemeLight] + + #: Colours as #rrggbb. One left out takes the preset's. + dark: NotRequired[BrandingAppearanceSetBodyThemeDark] + + +class BrandingAppearanceSetBody(TypedDict): + #: Theme this organization. Left out, the environment default. + organization_id: NotRequired[str | None] + + #: The theme. Both modes must be readable: text needs 4.5:1 against the background, muted text 3:1. + theme: BrandingAppearanceSetBodyTheme + + #: An https URL for the logo; null removes it, left out keeps it. + logo: NotRequired[str | None] + + +class BrandingWhitelabelGetQuery(TypedDict): + #: Read this organization's branding. Left out, the environment default. + organization_id: NotRequired[str] + + +class BrandingWhitelabelSetBodyPalette(TypedDict): + """Colours as hex (#0a2540) or oklch(…), by token: primary, accent, ring, foreground, background. Sent, it is the complete palette; a token left out or blank is unset.""" + + primary: NotRequired[str | None] + accent: NotRequired[str | None] + ring: NotRequired[str | None] + foreground: NotRequired[str | None] + background: NotRequired[str | None] + + +class BrandingWhitelabelSetBody(TypedDict): + #: Brand this organization. Left out, the environment default. + organization_id: NotRequired[str | None] + + #: Colours as hex (#0a2540) or oklch(…), by token: primary, accent, ring, foreground, background. Sent, it is the complete palette; a token left out or blank is unset. + palette: NotRequired[BrandingWhitelabelSetBodyPalette] + + #: The product name shown in place of Cbox ID; null to unset. + app_name: NotRequired[str | None] + + #: The sender name on mail; null to unset. + email_from_name: NotRequired[str | None] + + #: The welcome email body; blank or null to use the default. + email_template: NotRequired[str | None] + + +class DirectoriesConnectBodyCredentials(TypedDict): + """The provider's credentials. Write-only: sealed, never returned.""" + + + #: Google: the service account's JSON key, whole. + service_account_json: NotRequired[str] + + #: Google: the administrator the service account acts as. + admin_email: NotRequired[str] + + #: Entra: the directory (tenant) id. + tenant_id: NotRequired[str] + + #: Entra: the app registration's client id. + client_id: NotRequired[str] + + #: Entra: the app registration's client secret. + client_secret: NotRequired[str] + + +class DirectoriesConnectBody(TypedDict): + #: The organization whose people it provisions. + organization_id: str + + #: google_workspace or microsoft_entra. + provider: Literal["google_workspace", "microsoft_entra"] + + #: The provider's credentials. Write-only: sealed, never returned. + credentials: DirectoriesConnectBodyCredentials + + +class DirectoriesCreateBody(TypedDict): + #: The organization whose people it provisions. + organization_id: str + + #: What administrators call it: "Okta SCIM". + name: str + + +class DirectoriesDeleteBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class DirectoriesGetQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class DirectoriesGroupsListQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +DirectoriesGroupsListData: TypeAlias = 'list[DirectoryGroup]' + + +class DirectoriesGroupsMapBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: One of the directory's groups. + group_id: str + + #: A role the organization's own people may hold. + role_id: str + + #: true to map the group onto the role, false to unmap it. + mapped: bool + + +class DirectoriesListQuery(TypedDict): + #: Only this organization's. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +DirectoriesListData: TypeAlias = 'list[Directory]' + + +class DirectoriesStatusSetBody(TypedDict): + #: true to resume provisioning, false to pause it. + active: bool + + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class DirectoriesTokenRotateBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class DirectoriesUpdateBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: The new name. + name: str + + +class DomainsAddBody(TypedDict): + #: The domain, for example login.example.com. + domain: str + + +class EventsListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + #: Only these event types, e.g. `user.created`. + types: NotRequired[list[str]] + + #: Only this organization's events. + organization_id: NotRequired[str] + + +EventsListData: TypeAlias = 'list[DomainEvent]' + + +class FrontendKeysCreateBody(TypedDict): + #: What the key is for, so a person recognises it later. + name: str + + #: `test` or `live`; it is in the key's prefix. + mode: Literal["test", "live"] + + #: The exact origins (scheme, host, port) allowed to present the key, for example https://app.example.com. At least one. + origins: list[str] + + +class FrontendKeysListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +FrontendKeysListData: TypeAlias = 'list[FrontendKey]' + + +class FrontendKeysSetOriginsBody(TypedDict): + #: The exact origins (scheme, host, port) allowed to present the key, for example https://app.example.com. At least one. + origins: list[str] + + +class HooksCreateBody(TypedDict): + #: When it is called. + hook_point: Literal["token_minting", "post_login", "pre_registration", "post_registration", "pre_password_change", "post_password_change"] + + #: A public HTTPS URL, called in the middle of the operation. + url: str + + #: The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide. + organization_id: NotRequired[str | None] + + #: True to make it the environment's own, carrying EVERY organization's traffic. Send this or organization_id. + environment_wide: NotRequired[bool] + + +class HooksListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +HooksListData: TypeAlias = 'list[InlineHook]' + + +class HooksUpdateBody(TypedDict): + #: True to call it at its hook point; false to pause it. + active: bool + + +class InvitationsListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + + +InvitationsListData: TypeAlias = 'list[Invitation]' + + +class InvitationsSendBody(TypedDict): + email: str + role: NotRequired[AssignableMemberRole] + roles: NotRequired[list[str]] + client_id: NotRequired[str | None] + return_to: NotRequired[str | None] + inviter_name: NotRequired[str | None] + + +class KeysCreateBodyRequireApproval(TypedDict): + """Which of this key's actions need its owner's approval on their device first. From a key: never weaker than its own.""" + + + #: Every action at or above this danger waits for a person's approval. + min_danger: NotRequired[Literal["write", "destructive", "critical"] | None] + + #: These actions wait for approval whatever their danger. + actions: NotRequired[list[str]] + + +class KeysCreateBody(TypedDict): + name: str + + #: The scopes the key carries. From a key: a subset of its own. + scopes: list[str] + + #: When it stops working. From a key: required to be no later than the parent; defaults to the latest allowed. + expires_at: NotRequired[str | None] + + #: What it is for — shown beside it in the console. + description: NotRequired[str | None] + + #: Which of this key's actions need its owner's approval on their device first. From a key: never weaker than its own. + require_approval: NotRequired[KeysCreateBodyRequireApproval] + + +KeysCreateData: TypeAlias = dict[str, Any] + + +class KeysListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +KeysListItem: TypeAlias = dict[str, Any] + + +KeysListData: TypeAlias = list[dict[str, Any]] + + +class KeysRotateBody(TypedDict): + #: How long the old key keeps working. Default 24. + grace_hours: NotRequired[int] + + +KeysRotateData: TypeAlias = dict[str, Any] + + +class LegacyLoginProbeBody(TypedDict): + #: An address that exists in your old system — your own. + email: str + + +LegacyLoginProbeData: TypeAlias = dict[str, Any] + + +class LogStreamsCreateBody(TypedDict): + name: str + destination: Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json"] + + #: A public URL the entries are POSTed to. + endpoint_url: str + + #: How the endpoint is authenticated. Left out, the destination's default. + auth: NotRequired[Literal["none", "bearer", "splunk", "hmac"]] + + #: The bearer or Splunk token the endpoint expects. Left out with `hmac`, a key is generated and returned once. + secret: NotRequired[str | None] + + #: The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide. + organization_id: NotRequired[str | None] + + #: True to make it the environment's own, carrying EVERY organization's traffic. Send this or organization_id. + environment_wide: NotRequired[bool] + + +class LogStreamsListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +LogStreamsListData: TypeAlias = 'list[LogStream]' + + +class LogStreamsUpdateBody(TypedDict): + #: True to deliver; false to stop, keeping what is pending. + enabled: bool + + +class MembersAddBody(TypedDict): + user_id: NotRequired[str] + + #: Instead of `user_id`. + email: NotRequired[str] + role: NotRequired[AssignableMemberRole] + roles: NotRequired[list[str]] + + #: The app whose manifest keys `roles` are. + client_id: NotRequired[str | None] + + +class MembersListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + + +MembersListData: TypeAlias = 'list[Member]' + + +class MembersRolesGrantQuery(TypedDict): + #: The app whose manifest `key` `{role_id}` is. Not needed when `{role_id}` is an id. + client_id: NotRequired[str] + + +MembersRolesListData: TypeAlias = 'list[RoleAssignment]' + + +class MembersRolesRevokeQuery(TypedDict): + #: The app whose manifest `key` `{role_id}` is. Not needed when `{role_id}` is an id. + client_id: NotRequired[str] + + +class MembersUpdateBody(TypedDict): + role: AssignableMemberRole + + +class OrganizationsCreateBody(TypedDict): + name: str + + #: URL-safe; unique within the environment. Optional — left out, it is derived + #: from the name and made unique (`acme`, `acme-2`, …). **Send one if you + #: retry creates**: with your own slug a retried request is a `422 + #: slug_taken` you can recognise, not a second organization. + slug: NotRequired[str | None] + + #: An organization in this environment to place it under (`422 parent_not_found` otherwise). + parent_id: NotRequired[str | None] + + #: An existing user of this environment who becomes the organization's + #: **owner**, in the same transaction as the create (`422 user_not_found` + #: otherwise). Without it the organization starts with no owner; make one + #: later with `transfer-ownership`. + owner_user_id: NotRequired[str | None] + + #: `reseller` manages customer organizations beneath it in the tree; `customer` is a leaf tenant. + type: NotRequired[Literal["customer", "reseller"]] + + #: Free-form text values you keep on the organization. + metadata: NotRequired[dict[str, str] | None] + + +class OrganizationsDomainsAddBody(TypedDict): + #: A domain name — acme.com, not an address or a URL. + domain: str + + +class OrganizationsDomainsCaptureBody(TypedDict): + #: Whether the domain captures sign-ins. + enabled: bool + + +OrganizationsDomainsListData: TypeAlias = 'list[OrganizationDomain]' + + +class OrganizationsListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + q: NotRequired[str] + status: NotRequired[Literal["active", "suspended", "deleted"]] + + +OrganizationsListData: TypeAlias = 'list[Organization]' + + +class OrganizationsPortalLinksCreateBody(TypedDict): + #: What the link may set up: sso (connection and email domains), dsync (directory sync over SCIM), domain_verification, log_streams, certificate_renewal (a SAML connection's signing certificate), audit_logs (the organization's audit events, read-only, with CSV export). + intents: list[Literal["sso", "dsync", "domain_verification", "log_streams", "certificate_renewal", "audit_logs"]] + + #: How long the link may wait to be opened, in minutes — 5 to 10080 (a week). Left out, the deployment's default (30). + expires_in_minutes: NotRequired[int | None] + + #: Mail the link to this address — the customer's IT contact. Left out, nothing is sent. + email: NotRequired[str | None] + + #: The language of that mail. Left out, the environment's default language. + locale: NotRequired[Literal["en", "da", "de", "sv", "nb", "fr"] | None] + + +class OrganizationsTransferOwnershipBody(TypedDict): + user_id: str + + +class OrganizationsUpdateBody(TypedDict): + name: NotRequired[str] + + #: URL-safe; unique within the environment. + slug: NotRequired[str] + + #: Replaces the metadata as a whole; `null` or `{}` clears it. + metadata: NotRequired[dict[str, str] | None] + + +class PermissionsCreateBody(TypedDict): + #: The key: a feature and an action joined by a colon, like invoices:create. + name: str + description: NotRequired[str | None] + + #: The organization it belongs to; null for the environment's shared tier. + organization_id: NotRequired[str | None] + + #: Shared tier only: whether organizations may compose it into their own roles. Default false; always true on an organization's own. + tenant_assignable: NotRequired[bool] + + +class PermissionsListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + #: Also the permissions this organization authored for itself. + organization_id: NotRequired[str] + + #: Only the permissions this app declared. + client_id: NotRequired[str] + + #: Only permissions whose key or description contains this. + q: NotRequired[str] + + +PermissionsListData: TypeAlias = 'list[Permission]' + + +class PermissionsUpdateBody(TypedDict): + description: NotRequired[str | None] + + #: Shared tier only: whether organizations may compose it into their own roles. + tenant_assignable: NotRequired[bool] + + +class ProvisioningTargetsCreateBody(TypedDict): + #: The organization the target belongs to. Send this or environment_wide. + organization_id: NotRequired[str | None] + + #: True to make the target the environment's own rather than one organization's. Send this or organization_id. + environment_wide: NotRequired[bool] + + #: What administrators call it: "Slack", "Salesforce". + name: str + + #: The target's SCIM base URL. Must be a public address. + base_url: str + + #: bearer (a long-lived token the app issued you) or oauth2_client_credentials (a token fetched before each batch). + auth_scheme: Literal["bearer", "oauth2_client_credentials"] + + #: The bearer token, or the OAuth client secret. Write-only. + secret: str + + #: oauth2_client_credentials: the token endpoint. + token_url: NotRequired[str | None] + + #: oauth2_client_credentials: the client id. + client_id: NotRequired[str | None] + + #: oauth2_client_credentials: the scope to ask for, if the app wants one. + scope: NotRequired[str | None] + + +class ProvisioningTargetsDeleteBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class ProvisioningTargetsGetQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class ProvisioningTargetsListQuery(TypedDict): + #: Only this organization's. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +ProvisioningTargetsListData: TypeAlias = 'list[ProvisioningTarget]' + + +class ProvisioningTargetsStatusSetBody(TypedDict): + #: true to resume pushing, false to pause. + active: bool + + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class RolesCreateBody(TypedDict): + name: str + description: NotRequired[str | None] + + #: The organization it belongs to; null defines it for every organization in the environment. + organization_id: NotRequired[str | None] + + #: The app whose tokens it is stamped into; null for every app. + client_id: NotRequired[str | None] + + #: Permission ids to grant it from the start. + permissions: NotRequired[list[str]] + + +class RolesGetQuery(TypedDict): + #: The app whose manifest `key` `id` is. + client_id: NotRequired[str] + + +class RolesListQuery(TypedDict): + client_id: NotRequired[str] + organization_id: NotRequired[str] + + +RolesListData: TypeAlias = 'list[Role]' + + +class RolesUpdateBody(TypedDict): + name: NotRequired[str] + description: NotRequired[str | None] + + +class SamlAppsCreateBodyAttributeMappingsItem(TypedDict): + #: The attribute name the assertion emits: `email`, `displayName`. + attribute: str + + #: The person's field it is read from: `email`, `name`. + field: str + + +class SamlAppsCreateBody(TypedDict): + #: The application's SAML EntityID, unique in this environment. + entity_id: str + + #: Where assertions are posted: https (http only on localhost), absolute, no fragment. + acs_url: str + + #: The NameID format URN. Default emailAddress. + name_id_format: NotRequired[Literal["urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"]] + + #: The person's field the NameID is read from. Default `email`. + name_id_attribute: NotRequired[str] + + #: The attributes the assertion carries. Sent, it is the COMPLETE list afterwards. + attribute_mappings: NotRequired[list[SamlAppsCreateBodyAttributeMappingsItem]] + + #: Verify the application's signature on every AuthnRequest. Needs a certificate. + want_authn_requests_signed: NotRequired[bool] + + #: The application's signing certificate (PEM). Write-only; left out or null keeps the one on file. + certificate: NotRequired[str | None] + + #: The organization that owns the application: only its active members are signed in to it, and the assertion names it. Null makes it environment-wide — every person in the environment may sign in to it. + organization_id: NotRequired[str | None] + + +class SamlAppsListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +SamlAppsListData: TypeAlias = 'list[SamlApp]' + + +class SamlAppsUpdateBodyAttributeMappingsItem(TypedDict): + #: The attribute name the assertion emits: `email`, `displayName`. + attribute: str + + #: The person's field it is read from: `email`, `name`. + field: str + + +class SamlAppsUpdateBody(TypedDict): + #: The application's SAML EntityID, unique in this environment. + entity_id: NotRequired[str] + + #: Where assertions are posted: https (http only on localhost), absolute, no fragment. + acs_url: NotRequired[str] + + #: The NameID format URN. Default emailAddress. + name_id_format: NotRequired[Literal["urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"]] + + #: The person's field the NameID is read from. Default `email`. + name_id_attribute: NotRequired[str] + + #: The attributes the assertion carries. Sent, it is the COMPLETE list afterwards. + attribute_mappings: NotRequired[list[SamlAppsUpdateBodyAttributeMappingsItem]] + + #: Verify the application's signature on every AuthnRequest. Needs a certificate. + want_authn_requests_signed: NotRequired[bool] + + #: The application's signing certificate (PEM). Write-only; left out or null keeps the one on file. + certificate: NotRequired[str | None] + + #: The organization that owns the application: only its active members are signed in to it, and the assertion names it. Null makes it environment-wide — every person in the environment may sign in to it. + organization_id: NotRequired[str | None] + + +class SigninPolicyGetQuery(TypedDict): + #: Read one organization's rules. Left out, the environment baseline. + organization_id: NotRequired[str] + + +class SigninPolicyUpdateBody(TypedDict): + #: Write this organization's override. Left out, the environment baseline. + organization_id: NotRequired[str | None] + + #: Minimum password length. + min_length: NotRequired[int] + + #: Refuse passwords that appear in known breaches. + require_breach_check: NotRequired[bool] + + #: Days before a password must be changed; null for no limit. + max_age_days: NotRequired[int | None] + + #: How many previous passwords may not be reused. + reuse_history: NotRequired[int] + + #: Whether a second factor is off, optional or required. + mfa: NotRequired[Literal["off", "optional", "required"]] + + #: Whether SSO is off, preferred, or required — `required` refuses every other way in and signs out password sessions. + sso: NotRequired[Literal["off", "preferred", "required"]] + + #: Failed attempts before an account is locked; null for the deployment default (10 inside 15 minutes unless CBOX_ID_LOCKOUT_THRESHOLD says otherwise). + lockout_threshold: NotRequired[int | None] + + +class SigninSelfServiceSignupSetBody(TypedDict): + #: On: anyone may create an account and their own organization. Off: people join by invitation. + enabled: bool + + +SigninSelfServiceSignupSetData: TypeAlias = dict[str, Any] + + +class SigninSocialDeleteBody(TypedDict): + #: Only remove it if it is this organization's. + organization_id: NotRequired[str | None] + + +class SigninSocialListQuery(TypedDict): + #: Only this organization's providers. + organization_id: NotRequired[str] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +SigninSocialListData: TypeAlias = 'list[SocialProvider]' + + +class SigninSocialSetBodyParameters(TypedDict): + """What the chosen provider needs besides its client credentials — Microsoft's directory, Okta's domain, Apple's team id, key id and private key.""" + + + #: Directory (tenant) ID (Microsoft Entra ID). + directory: NotRequired[str | None] + + #: Okta domain (Okta). + domain: NotRequired[str | None] + + #: Keycloak host (Keycloak). + host: NotRequired[str | None] + + #: Key ID (Apple). + key_id: NotRequired[str | None] + + #: Private key (.p8) (Apple). + private_key: NotRequired[str | None] + + #: Realm (Keycloak). + realm: NotRequired[str | None] + + #: Team ID (Apple). + team_id: NotRequired[str | None] + + +class SigninSocialSetBody(TypedDict): + #: The organization whose sign-in page offers it. + organization_id: str + + #: The catalogue key: google, microsoft, okta, auth0, keycloak, gitlab, slack, github, discord, apple, facebook. + provider: str + + #: The client id from your own account with the provider. For Apple, the Services ID. + client_id: str + + #: The client secret. Write-only. Not used by Apple, which signs its own. + client_secret: NotRequired[str | None] + + #: What the chosen provider needs besides its client credentials — Microsoft's directory, Okta's domain, Apple's team id, key id and private key. + parameters: NotRequired[SigninSocialSetBodyParameters] + + +class SodPoliciesCreateBody(TypedDict): + #: The organization the rule belongs to. Send this or environment_wide. + organization_id: NotRequired[str | None] + + #: True to make the rule the environment's own rather than one organization's. Send this or organization_id. + environment_wide: NotRequired[bool] + + #: What the rule is for: "Payments maker/checker". + name: str + + #: Why the roles conflict. + description: NotRequired[str | None] + + #: The roles no one person may hold together; at least two. + role_ids: list[str] + + +class SodPoliciesDeleteBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SodPoliciesGetQuery(TypedDict): + #: Only if it binds this organization (its own, or environment-wide); anything else is a 404. + organization_id: NotRequired[str | None] + + +class SodPoliciesListQuery(TypedDict): + #: Only the rules binding this organization: its own and the environment-wide ones. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +SodPoliciesListData: TypeAlias = 'list[SodPolicy]' + + +class SodPoliciesStatusSetBody(TypedDict): + #: true to enforce the rule, false to stop enforcing it. + active: bool + + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsActivateBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsCertificatesActivateBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: The staged certificate's SHA-256 fingerprint, as the certificate list gives it. + fingerprint_sha256: str + + +class SsoConnectionsCertificatesListQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsCertificatesStageBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: The new signing certificate, PEM. Send this or metadata. + certificate: NotRequired[str | None] + + #: The identity provider's metadata XML, or the https URL it is published at. Send this or certificate. + metadata: NotRequired[str | None] + + +class SsoConnectionsCreateBody(TypedDict): + #: The organization the connection belongs to. Send this or environment_wide. + organization_id: NotRequired[str | None] + + #: True to make the connection the environment's own rather than one organization's. Send this or organization_id. + environment_wide: NotRequired[bool] + + #: What administrators call it: "Okta", "Entra ID". + name: str + + #: saml or oidc. The config fields of that type are required, unless pending_idp. + type: Literal["saml", "oidc"] + + #: True to create the draft before the identity provider's details are known: the answer's service_provider is what to paste into it. Complete it with update before activating. + pending_idp: NotRequired[bool] + + #: SAML: the identity provider's entity id. + idp_entity_id: NotRequired[str | None] + + #: SAML: the identity provider's single sign-on URL. + idp_sso_url: NotRequired[str | None] + + #: SAML: the identity provider's signing certificate (PEM). Write-only. + idp_x509cert: NotRequired[str | None] + + #: SAML: the entity id this environment presents as the service provider. Left out on create, this connection's own. + sp_entity_id: NotRequired[str | None] + + #: SAML: where the identity provider posts its assertions. Left out on create, this connection's own ACS URL. + sp_acs_url: NotRequired[str | None] + + #: OIDC: the provider's issuer URL; its endpoints are discovered from it. + issuer: NotRequired[str | None] + + #: OIDC: the client id registered at the provider. + client_id: NotRequired[str | None] + + #: OIDC: the client secret. Write-only. + client_secret: NotRequired[str | None] + + #: OIDC: the signing key. Write-only. + signing_key: NotRequired[str | None] + + +class SsoConnectionsDeleteBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsDisableBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsGetQuery(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsListQuery(TypedDict): + #: Only this organization's. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +SsoConnectionsListData: TypeAlias = 'list[SsoConnection]' + + +class SsoConnectionsRequireSsoBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoConnectionsUpdateBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + #: A new name. + name: NotRequired[str] + + #: SAML: the identity provider's entity id. + idp_entity_id: NotRequired[str | None] + + #: SAML: the identity provider's single sign-on URL. + idp_sso_url: NotRequired[str | None] + + #: SAML: the identity provider's signing certificate (PEM). Write-only. + idp_x509cert: NotRequired[str | None] + + #: SAML: the entity id this environment presents as the service provider. Left out on create, this connection's own. + sp_entity_id: NotRequired[str | None] + + #: SAML: where the identity provider posts its assertions. Left out on create, this connection's own ACS URL. + sp_acs_url: NotRequired[str | None] + + #: OIDC: the provider's issuer URL; its endpoints are discovered from it. + issuer: NotRequired[str | None] + + #: OIDC: the client id registered at the provider. + client_id: NotRequired[str | None] + + #: OIDC: the client secret. Write-only. + client_secret: NotRequired[str | None] + + #: OIDC: the signing key. Write-only. + signing_key: NotRequired[str | None] + + +class SsoDomainsCaptureBody(TypedDict): + #: true to capture the domain, false to release it. + capture: bool + + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoDomainsCreateBody(TypedDict): + #: The organization claiming it. + organization_id: str + + #: The email domain, e.g. acme.com. + domain: str + + +class SsoDomainsDeleteBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoDomainsListQuery(TypedDict): + #: Only this organization's. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +SsoDomainsListData: TypeAlias = 'list[SsoDomain]' + + +class SsoDomainsVerifyBody(TypedDict): + #: Only if it is this organization's; anything else is a 404. + organization_id: NotRequired[str | None] + + +class SsoSamlMetadataImportBody(TypedDict): + #: The IdP metadata XML, or an https URL it is published at. + metadata: str + + +class SupportSessionsStartBody(TypedDict): + #: The customer's user to act as. + user_id: str + organization_id: str + + #: The app's public client id. + client_id: str + + #: Your staff member. + actor_user_id: str + + #: Shown to the customer. + reason: str + ttl_minutes: NotRequired[int] + + #: Narrowed to the app's registration; omitted means all of it. + scopes: NotRequired[list[str]] + redirect_uri: NotRequired[str] + code_challenge: NotRequired[str] + nonce: NotRequired[str | None] + + +class TokenVaultGrantsCreateBody(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + #: The OAuth client id of the app that may lease it. + client_id: str + + +class TokenVaultGrantsDeleteBody(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + +class TokenVaultSecretsCreateBody(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + #: What it is: "Stripe live key". + name: str + + #: Whose credential it is: "stripe", "openai". + provider: str + + #: The credential. Write-only: sealed, never returned. + secret: str + + +class TokenVaultSecretsGetQuery(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + +class TokenVaultSecretsListQuery(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +TokenVaultSecretsListData: TypeAlias = 'list[TokenVaultSecret]' + + +class TokenVaultSecretsRevokeBody(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + +class TokenVaultSecretsRotateBody(TypedDict): + #: The organization whose vault this is. Left out: the environment's own secrets, a separate collection. + organization_id: NotRequired[str | None] + + #: The new value. Write-only: sealed, never returned. + secret: str + + +class UsersCreateBody(TypedDict): + email: str + name: NotRequired[str | None] + + #: Checked against this environment's password policy (`422 password_policy`). + password: NotRequired[str | None] + + #: Mail the new user a one-click sign-in link. + send_sign_in_link: NotRequired[bool] + + +class UsersEnvironmentRolesGetQuery(TypedDict): + #: The app whose manifest `key` `{role_id}` is. Not needed when `{role_id}` is an id. + client_id: NotRequired[str] + + +class UsersEnvironmentRolesGrantQuery(TypedDict): + #: The app whose manifest `key` `{role_id}` is. Not needed when `{role_id}` is an id. + client_id: NotRequired[str] + + +UsersEnvironmentRolesListData: TypeAlias = 'list[RoleAssignment]' + + +class UsersEnvironmentRolesRevokeQuery(TypedDict): + #: The app whose manifest `key` `{role_id}` is. Not needed when `{role_id}` is an id. + client_id: NotRequired[str] + + +class UsersListQuery(TypedDict): + limit: NotRequired[int] + + #: Opaque cursor — the `meta.next_cursor` from the previous page. + after: NotRequired[str] + email: NotRequired[str] + + #: A fragment of the address or the name. + q: NotRequired[str] + status: NotRequired[Literal["active", "disabled", "locked"]] + + +UsersListData: TypeAlias = 'list[User]' + + +class UsersPasswordSetBody(TypedDict): + #: The new password. Checked against this environment's password policy. + password: str + + #: Why — recorded on the audit trail. + reason: str + + #: The user must choose a new password at their next sign-in. Default true. + temporary: NotRequired[bool] + + #: A temporary password stops working after this many hours. 0 or absent: until changed. + expires_in_hours: NotRequired[int] + + #: How much existing access to end. Default `sessions_and_tokens`. + revoke: NotRequired[Literal["sessions_and_tokens", "sessions_only", "nothing"]] + + #: Mail the password to the user. Default true. + send_email: NotRequired[bool] + + +UsersSessionsListData: TypeAlias = 'list[UserSession]' + + +class UsersUpdateBody(TypedDict): + name: NotRequired[str | None] + + #: The new address. Its verification is cleared. + email: NotRequired[str] + + +class WebhooksCreateBody(TypedDict): + #: A public HTTPS URL. Signed deliveries are POSTed here. + url: str + + #: The events it receives. + event_types: list[Literal["user.created", "user.updated", "user.deactivated", "user.login", "user.reactivated", "identity.linked", "user.erased", "organization.created", "organization.suspended", "organization.reactivated", "organization.updated", "organization.deleted", "membership.created", "membership.updated", "membership.deleted", "invitation.created", "invitation.accepted", "invitation.revoked", "role.assigned", "role.unassigned", "role.assigned_everywhere", "role.unassigned_everywhere", "api_key.created", "api_key.revoked", "support_session.started", "directory.user.provisioned", "directory.user.deprovisioned", "directory.user.deactivated", "directory.group.membership_changed", "domain.added", "domain.removed", "domain.verified", "connection.activated", "connection.certificate_expiring", "entitlement.set", "entitlement.updated", "entitlement.revoked", "vault.grant.created", "vault.grant.revoked", "vault.secret.revoked", "governance.access.revoked"]] + + #: The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide. + organization_id: NotRequired[str | None] + + #: True to make it the environment's own, carrying EVERY organization's traffic. Send this or organization_id. + environment_wide: NotRequired[bool] + + +class WebhooksListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The `next_cursor` of the previous page. + after: NotRequired[str] + + +WebhooksListData: TypeAlias = 'list[Webhook]' + + +class WebhooksUpdateBody(TypedDict): + #: A public HTTPS URL. Left out, unchanged. + url: NotRequired[str] + + #: The COMPLETE set of events afterwards. Left out, unchanged. + event_types: NotRequired[list[str]] + + +# ── Operations ──────────────────────────────────────────────────────────────────────────────────── + + +#: Every operation of the Cbox ID — Environment Management API, keyed by action name. +ENVIRONMENT_OPERATIONS: Mapping[str, OperationSpec] = { + "access_reviews.close": OperationSpec(action="access_reviews.close", operation_id="access_reviews_close", method="POST", path="/access-reviews/{id}/close", path_params=("id",), scope="governance:write", danger="destructive", approval=True, body=True, pagination=None), + "access_reviews.create": OperationSpec(action="access_reviews.create", operation_id="access_reviews_create", method="POST", path="/access-reviews", path_params=(), scope="governance:write", danger="write", approval=True, body=True, pagination=None), + "access_reviews.get": OperationSpec(action="access_reviews.get", operation_id="access_reviews_get", method="GET", path="/access-reviews/{id}", path_params=("id",), scope="governance:read", danger="read", approval=True, body=False, pagination=None), + "access_reviews.items.decide": OperationSpec(action="access_reviews.items.decide", operation_id="access_reviews_items_decide", method="POST", path="/access-reviews/{id}/items/{item_id}", path_params=("id", "item_id"), scope="governance:write", danger="write", approval=True, body=True, pagination=None), + "access_reviews.items.list": OperationSpec(action="access_reviews.items.list", operation_id="access_reviews_items_list", method="GET", path="/access-reviews/{id}/items", path_params=("id",), scope="governance:read", danger="read", approval=True, body=False, pagination="cursor"), + "access_reviews.list": OperationSpec(action="access_reviews.list", operation_id="access_reviews_list", method="GET", path="/access-reviews", path_params=(), scope="governance:read", danger="read", approval=True, body=False, pagination="cursor"), + "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), + "api_keys.list": OperationSpec(action="api_keys.list", operation_id="api_keys_list", method="GET", path="/organizations/{organization_id}/api-keys", path_params=("organization_id",), scope="api_keys:read", danger=None, approval=True, body=False, pagination="cursor"), + "api_keys.revoke": OperationSpec(action="api_keys.revoke", operation_id="api_keys_revoke", method="DELETE", path="/api-keys/{id}", path_params=("id",), scope="api_keys:write", danger=None, approval=True, body=False, pagination=None), + "apis.create": OperationSpec(action=None, operation_id=None, method="POST", path="/apis", path_params=(), scope="apis:write", danger=None, approval=False, body=True, pagination=None), + "apis.delete": OperationSpec(action=None, operation_id=None, method="DELETE", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=False, body=False, pagination=None), + "apis.get": OperationSpec(action=None, operation_id=None, method="GET", path="/apis/{id}", path_params=("id",), scope="apis:read", danger=None, approval=False, body=False, pagination=None), + "apis.list": OperationSpec(action=None, operation_id=None, method="GET", path="/apis", path_params=(), scope="apis:read", danger=None, approval=False, body=False, pagination="cursor"), + "apis.scopes.define": OperationSpec(action="apis.scopes.define", operation_id="apis_scopes_define", method="PUT", path="/apis/{id}/scopes/{key}", path_params=("id", "key"), scope="apis:write", danger="write", approval=True, body=True, pagination=None), + "apis.scopes.remove": OperationSpec(action="apis.scopes.remove", operation_id="apis_scopes_remove", method="DELETE", path="/apis/{id}/scopes/{key}", path_params=("id", "key"), scope="apis:write", danger="destructive", approval=True, body=False, pagination=None), + "apis.update": OperationSpec(action=None, operation_id=None, method="PATCH", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=False, body=True, pagination=None), + "approvals.deny": OperationSpec(action="approvals.deny", operation_id="approvals_deny", method="POST", path="/agent-requests/{request_id}/deny", path_params=("request_id",), scope="approvals:write", danger="destructive", approval=True, body=False, pagination=None), + "approvals.list": OperationSpec(action="approvals.list", operation_id="approvals_list", method="GET", path="/agent-requests", path_params=(), scope="approvals:read", danger="read", approval=True, body=False, pagination="cursor"), + "apps.blueprint": OperationSpec(action="apps.blueprint", operation_id="apps_blueprint", method="GET", path="/apps/{id}/blueprint", path_params=("id",), scope="apps:read", danger=None, approval=False, body=False, pagination=None), + "apps.copy": OperationSpec(action="apps.copy", operation_id="apps_copy", method="POST", path="/apps/{id}/copy", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), + "apps.create": OperationSpec(action="apps.create", operation_id="apps_create", method="POST", path="/apps", path_params=(), scope="apps:write", danger="critical", approval=False, body=True, pagination=None), + "apps.delete": OperationSpec(action="apps.delete", operation_id="apps_delete", method="DELETE", path="/apps/{id}", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=False, pagination=None), + "apps.get": OperationSpec(action="apps.get", operation_id="apps_get", method="GET", path="/apps/{id}", path_params=("id",), scope="apps:read", danger="read", approval=True, body=False, pagination=None), + "apps.list": OperationSpec(action="apps.list", operation_id="apps_list", method="GET", path="/apps", path_params=(), scope="apps:read", danger=None, approval=False, body=False, pagination="cursor"), + "apps.manifest.set": OperationSpec(action="apps.manifest.set", operation_id="apps_manifest_set", method="PUT", path="/apps/{id}/manifest", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "apps.manifest.sync": OperationSpec(action="apps.manifest.sync", operation_id="apps_manifest_sync", method="POST", path="/apps/{id}/manifest/sync", path_params=("id",), scope="apps:write", danger="write", approval=True, body=False, pagination=None), + "apps.scopes.set": OperationSpec(action="apps.scopes.set", operation_id="apps_scopes_set", method="PUT", path="/apps/{id}/scopes", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "apps.secrets.list": OperationSpec(action="apps.secrets.list", operation_id="apps_secrets_list", method="GET", path="/apps/{id}/secrets", path_params=("id",), scope="apps:read", danger=None, approval=False, body=False, pagination=None), + "apps.secrets.revoke": OperationSpec(action="apps.secrets.revoke", operation_id="apps_secrets_revoke", method="DELETE", path="/apps/{id}/secrets/{secret_id}", path_params=("id", "secret_id"), scope="apps:write", danger="critical", approval=True, body=False, pagination=None), + "apps.secrets.rotate": OperationSpec(action="apps.secrets.rotate", operation_id="apps_secrets_rotate", method="POST", path="/apps/{id}/secrets", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), + "apps.settings.api_key_prefix": OperationSpec(action="apps.settings.api_key_prefix", operation_id="apps_settings_api_key_prefix", method="PUT", path="/apps/{id}/settings/api-key-prefix", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "apps.settings.backchannel_logout": OperationSpec(action="apps.settings.backchannel_logout", operation_id="apps_settings_backchannel_logout", method="PUT", path="/apps/{id}/settings/backchannel-logout", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "apps.settings.token_exchange": OperationSpec(action="apps.settings.token_exchange", operation_id="apps_settings_token_exchange", method="PUT", path="/apps/{id}/settings/token-exchange", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "apps.settings.token_lifetime": OperationSpec(action="apps.settings.token_lifetime", operation_id="apps_settings_token_lifetime", method="PUT", path="/apps/{id}/settings/token-lifetime", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "apps.update": OperationSpec(action="apps.update", operation_id="apps_update", method="PATCH", path="/apps/{id}", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), + "audit.list": OperationSpec(action="audit.list", operation_id="audit_list", method="GET", path="/audit-log", path_params=(), scope="audit:read", danger="read", approval=True, body=False, pagination="cursor"), + "audit_logs.events.create": OperationSpec(action="audit_logs.events.create", operation_id="audit_logs_events_create", method="POST", path="/audit-logs/events", path_params=(), scope="audit_logs:write", danger="write", approval=True, body=True, pagination=None), + "audit_logs.events.list": OperationSpec(action="audit_logs.events.list", operation_id="audit_logs_events_list", method="GET", path="/audit-logs/events", path_params=(), scope="audit_logs:read", danger="read", approval=True, body=False, pagination="cursor"), + "audit_logs.exports.create": OperationSpec(action="audit_logs.exports.create", operation_id="audit_logs_exports_create", method="POST", path="/audit-logs/exports", path_params=(), scope="audit_logs:export", danger="write", approval=True, body=True, pagination=None), + "audit_logs.exports.get": OperationSpec(action="audit_logs.exports.get", operation_id="audit_logs_exports_get", method="GET", path="/audit-logs/exports/{id}", path_params=("id",), scope="audit_logs:read", danger="read", approval=True, body=False, pagination=None), + "audit_logs.schemas.create": OperationSpec(action="audit_logs.schemas.create", operation_id="audit_logs_schemas_create", method="POST", path="/audit-logs/schemas", path_params=(), scope="audit_logs:manage", danger="write", approval=True, body=True, pagination=None), + "audit_logs.schemas.delete": OperationSpec(action="audit_logs.schemas.delete", operation_id="audit_logs_schemas_delete", method="DELETE", path="/audit-logs/schemas/{action}", path_params=("action",), scope="audit_logs:manage", danger="destructive", approval=True, body=False, pagination=None), + "audit_logs.schemas.get": OperationSpec(action="audit_logs.schemas.get", operation_id="audit_logs_schemas_get", method="GET", path="/audit-logs/schemas/{action}", path_params=("action",), scope="audit_logs:read", danger="read", approval=True, body=False, pagination=None), + "audit_logs.schemas.list": OperationSpec(action="audit_logs.schemas.list", operation_id="audit_logs_schemas_list", method="GET", path="/audit-logs/schemas", path_params=(), scope="audit_logs:read", danger="read", approval=True, body=False, pagination="cursor"), + "audit_logs.schemas.update": OperationSpec(action="audit_logs.schemas.update", operation_id="audit_logs_schemas_update", method="PUT", path="/audit-logs/schemas/{action}", path_params=("action",), scope="audit_logs:manage", danger="write", approval=True, body=True, pagination=None), + "audit_logs.settings.get": OperationSpec(action="audit_logs.settings.get", operation_id="audit_logs_settings_get", method="GET", path="/audit-logs/settings", path_params=(), scope="audit_logs:read", danger="read", approval=True, body=False, pagination=None), + "audit_logs.settings.update": OperationSpec(action="audit_logs.settings.update", operation_id="audit_logs_settings_update", method="PATCH", path="/audit-logs/settings", path_params=(), scope="audit_logs:manage", danger="destructive", approval=True, body=True, pagination=None), + "audit_logs.verify": OperationSpec(action="audit_logs.verify", operation_id="audit_logs_verify", method="GET", path="/audit-logs/verify", path_params=(), scope="audit_logs:read", danger="read", approval=True, body=False, pagination=None), + "branding.appearance.get": OperationSpec(action="branding.appearance.get", operation_id="branding_appearance_get", method="GET", path="/branding/appearance", path_params=(), scope="branding:read", danger="read", approval=True, body=False, pagination=None), + "branding.appearance.set": OperationSpec(action="branding.appearance.set", operation_id="branding_appearance_set", method="PUT", path="/branding/appearance", path_params=(), scope="branding:write", danger="write", approval=True, body=True, pagination=None), + "branding.whitelabel.get": OperationSpec(action="branding.whitelabel.get", operation_id="branding_whitelabel_get", method="GET", path="/branding/whitelabel", path_params=(), scope="branding:read", danger="read", approval=True, body=False, pagination=None), + "branding.whitelabel.set": OperationSpec(action="branding.whitelabel.set", operation_id="branding_whitelabel_set", method="PUT", path="/branding/whitelabel", path_params=(), scope="branding:write", danger="write", approval=True, body=True, pagination=None), + "directories.connect": OperationSpec(action="directories.connect", operation_id="directories_connect", method="POST", path="/directories/connect", path_params=(), scope="directory_sync:write", danger="critical", approval=True, body=True, pagination=None), + "directories.create": OperationSpec(action="directories.create", operation_id="directories_create", method="POST", path="/directories", path_params=(), scope="directory_sync:write", danger="critical", approval=True, body=True, pagination=None), + "directories.delete": OperationSpec(action="directories.delete", operation_id="directories_delete", method="DELETE", path="/directories/{id}", path_params=("id",), scope="directory_sync:write", danger="destructive", approval=True, body=True, pagination=None), + "directories.get": OperationSpec(action="directories.get", operation_id="directories_get", method="GET", path="/directories/{id}", path_params=("id",), scope="directory_sync:read", danger="read", approval=True, body=False, pagination=None), + "directories.groups.list": OperationSpec(action="directories.groups.list", operation_id="directories_groups_list", method="GET", path="/directories/{id}/groups", path_params=("id",), scope="directory_sync:read", danger="read", approval=True, body=False, pagination="cursor"), + "directories.groups.map": OperationSpec(action="directories.groups.map", operation_id="directories_groups_map", method="POST", path="/directories/{id}/group-roles", path_params=("id",), scope="directory_sync:write", danger="write", approval=True, body=True, pagination=None), + "directories.list": OperationSpec(action="directories.list", operation_id="directories_list", method="GET", path="/directories", path_params=(), scope="directory_sync:read", danger="read", approval=True, body=False, pagination="cursor"), + "directories.status.set": OperationSpec(action="directories.status.set", operation_id="directories_status_set", method="POST", path="/directories/{id}/status", path_params=("id",), scope="directory_sync:write", danger="write", approval=True, body=True, pagination=None), + "directories.token.rotate": OperationSpec(action="directories.token.rotate", operation_id="directories_token_rotate", method="POST", path="/directories/{id}/rotate", path_params=("id",), scope="directory_sync:write", danger="critical", approval=True, body=True, pagination=None), + "directories.update": OperationSpec(action="directories.update", operation_id="directories_update", method="PATCH", path="/directories/{id}", path_params=("id",), scope="directory_sync:write", danger="write", approval=True, body=True, pagination=None), + "domains.add": OperationSpec(action="domains.add", operation_id="domains_add", method="POST", path="/domains", path_params=(), scope="domains:write", danger="write", approval=True, body=True, pagination=None), + "domains.get": OperationSpec(action="domains.get", operation_id="domains_get", method="GET", path="/domains", path_params=(), scope="domains:read", danger="read", approval=True, body=False, pagination=None), + "domains.remove": OperationSpec(action="domains.remove", operation_id="domains_remove", method="DELETE", path="/domains", path_params=(), scope="domains:write", danger="destructive", approval=True, body=False, pagination=None), + "domains.verify": OperationSpec(action="domains.verify", operation_id="domains_verify", method="POST", path="/domains/verify", path_params=(), scope="domains:write", danger="write", approval=True, body=False, pagination=None), + "events.list": OperationSpec(action="events.list", operation_id="events_list", method="GET", path="/events", path_params=(), scope="events:read", danger="read", approval=True, body=False, pagination="cursor"), + "frontend_keys.create": OperationSpec(action="frontend_keys.create", operation_id="frontend_keys_create", method="POST", path="/frontend-keys", path_params=(), scope="frontend_keys:write", danger="write", approval=True, body=True, pagination=None), + "frontend_keys.list": OperationSpec(action="frontend_keys.list", operation_id="frontend_keys_list", method="GET", path="/frontend-keys", path_params=(), scope="frontend_keys:read", danger="read", approval=True, body=False, pagination="cursor"), + "frontend_keys.revoke": OperationSpec(action="frontend_keys.revoke", operation_id="frontend_keys_revoke", method="DELETE", path="/frontend-keys/{id}", path_params=("id",), scope="frontend_keys:write", danger="destructive", approval=True, body=False, pagination=None), + "frontend_keys.set_origins": OperationSpec(action="frontend_keys.set_origins", operation_id="frontend_keys_set_origins", method="PUT", path="/frontend-keys/{id}/origins", path_params=("id",), scope="frontend_keys:write", danger="write", approval=True, body=True, pagination=None), + "hooks.create": OperationSpec(action="hooks.create", operation_id="hooks_create", method="POST", path="/hooks", path_params=(), scope="hooks:write", danger="critical", approval=True, body=True, pagination=None), + "hooks.delete": OperationSpec(action="hooks.delete", operation_id="hooks_delete", method="DELETE", path="/hooks/{id}", path_params=("id",), scope="hooks:write", danger="destructive", approval=True, body=False, pagination=None), + "hooks.get": OperationSpec(action="hooks.get", operation_id="hooks_get", method="GET", path="/hooks/{id}", path_params=("id",), scope="hooks:read", danger="read", approval=True, body=False, pagination=None), + "hooks.list": OperationSpec(action="hooks.list", operation_id="hooks_list", method="GET", path="/hooks", path_params=(), scope="hooks:read", danger="read", approval=True, body=False, pagination="cursor"), + "hooks.update": OperationSpec(action="hooks.update", operation_id="hooks_update", method="PATCH", path="/hooks/{id}", path_params=("id",), scope="hooks:write", danger="critical", approval=True, body=True, pagination=None), + "invitations.list": OperationSpec(action="invitations.list", operation_id="invitations_list", method="GET", path="/organizations/{organization_id}/invitations", path_params=("organization_id",), scope="invitations:read", danger=None, approval=True, body=False, pagination="cursor"), + "invitations.resend": OperationSpec(action="invitations.resend", operation_id="invitations_resend", method="POST", path="/organizations/{organization_id}/invitations/{invitation_id}/resend", path_params=("organization_id", "invitation_id"), scope="invitations:write", danger=None, approval=True, body=False, pagination=None), + "invitations.revoke": OperationSpec(action="invitations.revoke", operation_id="invitations_revoke", method="DELETE", path="/organizations/{organization_id}/invitations/{invitation_id}", path_params=("organization_id", "invitation_id"), scope="invitations:write", danger=None, approval=True, body=False, pagination=None), + "invitations.send": OperationSpec(action="invitations.send", operation_id="invitations_send", method="POST", path="/organizations/{organization_id}/invitations", path_params=("organization_id",), scope="invitations:write", danger=None, approval=True, body=True, pagination=None), + "keys.create": OperationSpec(action="keys.create", operation_id="keys_create", method="POST", path="/keys", path_params=(), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), + "keys.list": OperationSpec(action="keys.list", operation_id="keys_list", method="GET", path="/keys", path_params=(), scope="keys:read", danger="read", approval=True, body=False, pagination="cursor"), + "keys.revoke": OperationSpec(action="keys.revoke", operation_id="keys_revoke", method="DELETE", path="/keys/{id}", path_params=("id",), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), + "keys.rotate": OperationSpec(action="keys.rotate", operation_id="keys_rotate", method="POST", path="/keys/{id}/rotate", path_params=("id",), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), + "legacy_login.approve": OperationSpec(action="legacy_login.approve", operation_id="legacy_login_approve", method="POST", path="/legacy-login/approve", path_params=(), scope="signin:write", danger="critical", approval=True, body=False, pagination=None), + "legacy_login.get": OperationSpec(action="legacy_login.get", operation_id="legacy_login_get", method="GET", path="/legacy-login", path_params=(), scope="signin:read", danger="read", approval=True, body=False, pagination=None), + "legacy_login.probe": OperationSpec(action="legacy_login.probe", operation_id="legacy_login_probe", method="POST", path="/legacy-login/probe", path_params=(), scope="signin:write", danger="write", approval=True, body=True, pagination=None), + "legacy_login.revoke": OperationSpec(action="legacy_login.revoke", operation_id="legacy_login_revoke", method="POST", path="/legacy-login/revoke", path_params=(), scope="signin:write", danger="critical", approval=True, body=False, pagination=None), + "log_streams.create": OperationSpec(action="log_streams.create", operation_id="log_streams_create", method="POST", path="/log-streams", path_params=(), scope="log_streams:write", danger="critical", approval=True, body=True, pagination=None), + "log_streams.delete": OperationSpec(action="log_streams.delete", operation_id="log_streams_delete", method="DELETE", path="/log-streams/{id}", path_params=("id",), scope="log_streams:write", danger="destructive", approval=True, body=False, pagination=None), + "log_streams.get": OperationSpec(action="log_streams.get", operation_id="log_streams_get", method="GET", path="/log-streams/{id}", path_params=("id",), scope="log_streams:read", danger="read", approval=True, body=False, pagination=None), + "log_streams.list": OperationSpec(action="log_streams.list", operation_id="log_streams_list", method="GET", path="/log-streams", path_params=(), scope="log_streams:read", danger="read", approval=True, body=False, pagination="cursor"), + "log_streams.test": OperationSpec(action="log_streams.test", operation_id="log_streams_test", method="POST", path="/log-streams/{id}/test", path_params=("id",), scope="log_streams:write", danger="write", approval=True, body=False, pagination=None), + "log_streams.update": OperationSpec(action="log_streams.update", operation_id="log_streams_update", method="PATCH", path="/log-streams/{id}", path_params=("id",), scope="log_streams:write", danger="critical", approval=True, body=True, pagination=None), + "members.add": OperationSpec(action="members.add", operation_id="members_add", method="POST", path="/organizations/{organization_id}/members", path_params=("organization_id",), scope="members:write", danger=None, approval=True, body=True, pagination=None), + "members.list": OperationSpec(action="members.list", operation_id="members_list", method="GET", path="/organizations/{organization_id}/members", path_params=("organization_id",), scope="members:read", danger=None, approval=True, body=False, pagination="cursor"), + "members.remove": OperationSpec(action="members.remove", operation_id="members_remove", method="DELETE", path="/organizations/{organization_id}/members/{user_id}", path_params=("organization_id", "user_id"), scope="members:write", danger=None, approval=True, body=False, pagination=None), + "members.roles.grant": OperationSpec(action="members.roles.grant", operation_id="members_roles_grant", method="PUT", path="/organizations/{organization_id}/members/{user_id}/roles/{role_id}", path_params=("organization_id", "user_id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), + "members.roles.list": OperationSpec(action="members.roles.list", operation_id="members_roles_list", method="GET", path="/organizations/{organization_id}/members/{user_id}/roles", path_params=("organization_id", "user_id"), scope="roles:read", danger=None, approval=True, body=False, pagination=None), + "members.roles.revoke": OperationSpec(action="members.roles.revoke", operation_id="members_roles_revoke", method="DELETE", path="/organizations/{organization_id}/members/{user_id}/roles/{role_id}", path_params=("organization_id", "user_id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), + "members.update": OperationSpec(action="members.update", operation_id="members_update", method="PATCH", path="/organizations/{organization_id}/members/{user_id}", path_params=("organization_id", "user_id"), scope="members:write", danger=None, approval=True, body=True, pagination=None), + "organizations.create": OperationSpec(action="organizations.create", operation_id="organizations_create", method="POST", path="/organizations", path_params=(), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), + "organizations.delete": OperationSpec(action="organizations.delete", operation_id="organizations_delete", method="DELETE", path="/organizations/{id}", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=False, pagination=None), + "organizations.domains.add": OperationSpec(action="organizations.domains.add", operation_id="organizations_domains_add", method="POST", path="/organizations/{organization_id}/domains", path_params=("organization_id",), scope="organizations:write", danger="write", approval=True, body=True, pagination=None), + "organizations.domains.capture": OperationSpec(action="organizations.domains.capture", operation_id="organizations_domains_capture", method="PUT", path="/organizations/{organization_id}/domains/{domain_id}/capture", path_params=("organization_id", "domain_id"), scope="organizations:write", danger="critical", approval=True, body=True, pagination=None), + "organizations.domains.list": OperationSpec(action="organizations.domains.list", operation_id="organizations_domains_list", method="GET", path="/organizations/{organization_id}/domains", path_params=("organization_id",), scope="organizations:read", danger=None, approval=True, body=False, pagination=None), + "organizations.domains.remove": OperationSpec(action="organizations.domains.remove", operation_id="organizations_domains_remove", method="DELETE", path="/organizations/{organization_id}/domains/{domain_id}", path_params=("organization_id", "domain_id"), scope="organizations:write", danger="destructive", approval=True, body=False, pagination=None), + "organizations.domains.verify": OperationSpec(action="organizations.domains.verify", operation_id="organizations_domains_verify", method="POST", path="/organizations/{organization_id}/domains/{domain_id}/verify", path_params=("organization_id", "domain_id"), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), + "organizations.get": OperationSpec(action="organizations.get", operation_id="organizations_get", method="GET", path="/organizations/{id}", path_params=("id",), scope="organizations:read", danger=None, approval=True, body=False, pagination=None), + "organizations.list": OperationSpec(action="organizations.list", operation_id="organizations_list", method="GET", path="/organizations", path_params=(), scope="organizations:read", danger=None, approval=True, body=False, pagination="cursor"), + "organizations.portal_links.create": OperationSpec(action="organizations.portal_links.create", operation_id="organizations_portal_links_create", method="POST", path="/organizations/{organization_id}/portal-links", path_params=("organization_id",), scope="portal_links:write", danger="critical", approval=True, body=True, pagination=None), + "organizations.reactivate": OperationSpec(action="organizations.reactivate", operation_id="organizations_reactivate", method="POST", path="/organizations/{id}/reactivate", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), + "organizations.suspend": OperationSpec(action="organizations.suspend", operation_id="organizations_suspend", method="POST", path="/organizations/{id}/suspend", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), + "organizations.transfer_ownership": OperationSpec(action="organizations.transfer_ownership", operation_id="organizations_transfer_ownership", method="POST", path="/organizations/{id}/transfer-ownership", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), + "organizations.update": OperationSpec(action="organizations.update", operation_id="organizations_update", method="PATCH", path="/organizations/{id}", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), + "permissions.create": OperationSpec(action="permissions.create", operation_id="permissions_create", method="POST", path="/permissions", path_params=(), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), + "permissions.delete": OperationSpec(action="permissions.delete", operation_id="permissions_delete", method="DELETE", path="/permissions/{id}", path_params=("id",), scope="role_definitions:write", danger="destructive", approval=True, body=False, pagination=None), + "permissions.list": OperationSpec(action="permissions.list", operation_id="permissions_list", method="GET", path="/permissions", path_params=(), scope="roles:read", danger="read", approval=True, body=False, pagination="cursor"), + "permissions.update": OperationSpec(action="permissions.update", operation_id="permissions_update", method="PATCH", path="/permissions/{id}", path_params=("id",), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), + "provisioning.targets.create": OperationSpec(action="provisioning.targets.create", operation_id="provisioning_targets_create", method="POST", path="/provisioning-targets", path_params=(), scope="provisioning:write", danger="critical", approval=True, body=True, pagination=None), + "provisioning.targets.delete": OperationSpec(action="provisioning.targets.delete", operation_id="provisioning_targets_delete", method="DELETE", path="/provisioning-targets/{id}", path_params=("id",), scope="provisioning:write", danger="destructive", approval=True, body=True, pagination=None), + "provisioning.targets.get": OperationSpec(action="provisioning.targets.get", operation_id="provisioning_targets_get", method="GET", path="/provisioning-targets/{id}", path_params=("id",), scope="provisioning:read", danger="read", approval=True, body=False, pagination=None), + "provisioning.targets.list": OperationSpec(action="provisioning.targets.list", operation_id="provisioning_targets_list", method="GET", path="/provisioning-targets", path_params=(), scope="provisioning:read", danger="read", approval=True, body=False, pagination="cursor"), + "provisioning.targets.status.set": OperationSpec(action="provisioning.targets.status.set", operation_id="provisioning_targets_status_set", method="POST", path="/provisioning-targets/{id}/status", path_params=("id",), scope="provisioning:write", danger="write", approval=True, body=True, pagination=None), + "roles.create": OperationSpec(action="roles.create", operation_id="roles_create", method="POST", path="/roles", path_params=(), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), + "roles.delete": OperationSpec(action="roles.delete", operation_id="roles_delete", method="DELETE", path="/roles/{id}", path_params=("id",), scope="role_definitions:write", danger="destructive", approval=True, body=False, pagination=None), + "roles.get": OperationSpec(action="roles.get", operation_id="roles_get", method="GET", path="/roles/{id}", path_params=("id",), scope="roles:read", danger="read", approval=True, body=False, pagination=None), + "roles.list": OperationSpec(action="roles.list", operation_id="roles_list", method="GET", path="/roles", path_params=(), scope="roles:read", danger=None, approval=True, body=False, pagination=None), + "roles.permissions.grant": OperationSpec(action="roles.permissions.grant", operation_id="roles_permissions_grant", method="PUT", path="/roles/{id}/permissions/{permission_id}", path_params=("id", "permission_id"), scope="role_definitions:write", danger="write", approval=True, body=False, pagination=None), + "roles.permissions.revoke": OperationSpec(action="roles.permissions.revoke", operation_id="roles_permissions_revoke", method="DELETE", path="/roles/{id}/permissions/{permission_id}", path_params=("id", "permission_id"), scope="role_definitions:write", danger="destructive", approval=True, body=False, pagination=None), + "roles.update": OperationSpec(action="roles.update", operation_id="roles_update", method="PATCH", path="/roles/{id}", path_params=("id",), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), + "saml_apps.create": OperationSpec(action="saml_apps.create", operation_id="saml_apps_create", method="POST", path="/saml-apps", path_params=(), scope="saml_apps:write", danger="critical", approval=True, body=True, pagination=None), + "saml_apps.delete": OperationSpec(action="saml_apps.delete", operation_id="saml_apps_delete", method="DELETE", path="/saml-apps/{id}", path_params=("id",), scope="saml_apps:write", danger="critical", approval=True, body=False, pagination=None), + "saml_apps.get": OperationSpec(action="saml_apps.get", operation_id="saml_apps_get", method="GET", path="/saml-apps/{id}", path_params=("id",), scope="saml_apps:read", danger="read", approval=True, body=False, pagination=None), + "saml_apps.list": OperationSpec(action="saml_apps.list", operation_id="saml_apps_list", method="GET", path="/saml-apps", path_params=(), scope="saml_apps:read", danger="read", approval=True, body=False, pagination="cursor"), + "saml_apps.update": OperationSpec(action="saml_apps.update", operation_id="saml_apps_update", method="PATCH", path="/saml-apps/{id}", path_params=("id",), scope="saml_apps:write", danger="critical", approval=True, body=True, pagination=None), + "signin.policy.get": OperationSpec(action="signin.policy.get", operation_id="signin_policy_get", method="GET", path="/sign-in/policy", path_params=(), scope="signin:read", danger="read", approval=True, body=False, pagination=None), + "signin.policy.inherit": OperationSpec(action="signin.policy.inherit", operation_id="signin_policy_inherit", method="DELETE", path="/sign-in/policy/organizations/{organization_id}", path_params=("organization_id",), scope="signin:write", danger="critical", approval=True, body=False, pagination=None), + "signin.policy.update": OperationSpec(action="signin.policy.update", operation_id="signin_policy_update", method="PATCH", path="/sign-in/policy", path_params=(), scope="signin:write", danger="critical", approval=True, body=True, pagination=None), + "signin.self_service_signup.set": OperationSpec(action="signin.self_service_signup.set", operation_id="signin_self_service_signup_set", method="PUT", path="/sign-in/self-service-signup", path_params=(), scope="signin:write", danger="critical", approval=True, body=True, pagination=None), + "signin.social.delete": OperationSpec(action="signin.social.delete", operation_id="signin_social_delete", method="DELETE", path="/sign-in/social-providers/{id}", path_params=("id",), scope="signin:write", danger="critical", approval=True, body=True, pagination=None), + "signin.social.list": OperationSpec(action="signin.social.list", operation_id="signin_social_list", method="GET", path="/sign-in/social-providers", path_params=(), scope="signin:read", danger="read", approval=True, body=False, pagination="cursor"), + "signin.social.set": OperationSpec(action="signin.social.set", operation_id="signin_social_set", method="POST", path="/sign-in/social-providers", path_params=(), scope="signin:write", danger="critical", approval=True, body=True, pagination=None), + "sod_policies.create": OperationSpec(action="sod_policies.create", operation_id="sod_policies_create", method="POST", path="/sod-policies", path_params=(), scope="governance:write", danger="write", approval=True, body=True, pagination=None), + "sod_policies.delete": OperationSpec(action="sod_policies.delete", operation_id="sod_policies_delete", method="DELETE", path="/sod-policies/{id}", path_params=("id",), scope="governance:write", danger="critical", approval=True, body=True, pagination=None), + "sod_policies.get": OperationSpec(action="sod_policies.get", operation_id="sod_policies_get", method="GET", path="/sod-policies/{id}", path_params=("id",), scope="governance:read", danger="read", approval=True, body=False, pagination=None), + "sod_policies.list": OperationSpec(action="sod_policies.list", operation_id="sod_policies_list", method="GET", path="/sod-policies", path_params=(), scope="governance:read", danger="read", approval=True, body=False, pagination="cursor"), + "sod_policies.status.set": OperationSpec(action="sod_policies.status.set", operation_id="sod_policies_status_set", method="POST", path="/sod-policies/{id}/status", path_params=("id",), scope="governance:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.activate": OperationSpec(action="sso.connections.activate", operation_id="sso_connections_activate", method="POST", path="/sso/connections/{id}/activate", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.certificates.activate": OperationSpec(action="sso.connections.certificates.activate", operation_id="sso_connections_certificates_activate", method="POST", path="/sso/connections/{id}/certificates/activate", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.certificates.list": OperationSpec(action="sso.connections.certificates.list", operation_id="sso_connections_certificates_list", method="GET", path="/sso/connections/{id}/certificates", path_params=("id",), scope="sso:read", danger="read", approval=True, body=False, pagination=None), + "sso.connections.certificates.stage": OperationSpec(action="sso.connections.certificates.stage", operation_id="sso_connections_certificates_stage", method="POST", path="/sso/connections/{id}/certificates", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.create": OperationSpec(action="sso.connections.create", operation_id="sso_connections_create", method="POST", path="/sso/connections", path_params=(), scope="sso:write", danger="write", approval=True, body=True, pagination=None), + "sso.connections.delete": OperationSpec(action="sso.connections.delete", operation_id="sso_connections_delete", method="DELETE", path="/sso/connections/{id}", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.disable": OperationSpec(action="sso.connections.disable", operation_id="sso_connections_disable", method="POST", path="/sso/connections/{id}/disable", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.get": OperationSpec(action="sso.connections.get", operation_id="sso_connections_get", method="GET", path="/sso/connections/{id}", path_params=("id",), scope="sso:read", danger="read", approval=True, body=False, pagination=None), + "sso.connections.list": OperationSpec(action="sso.connections.list", operation_id="sso_connections_list", method="GET", path="/sso/connections", path_params=(), scope="sso:read", danger="read", approval=True, body=False, pagination="cursor"), + "sso.connections.require_sso": OperationSpec(action="sso.connections.require_sso", operation_id="sso_connections_require_sso", method="POST", path="/sso/connections/{id}/require-sso", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.connections.update": OperationSpec(action="sso.connections.update", operation_id="sso_connections_update", method="PATCH", path="/sso/connections/{id}", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.domains.capture": OperationSpec(action="sso.domains.capture", operation_id="sso_domains_capture", method="POST", path="/sso/domains/{id}/capture", path_params=("id",), scope="sso:write", danger="critical", approval=True, body=True, pagination=None), + "sso.domains.create": OperationSpec(action="sso.domains.create", operation_id="sso_domains_create", method="POST", path="/sso/domains", path_params=(), scope="sso:write", danger="write", approval=True, body=True, pagination=None), + "sso.domains.delete": OperationSpec(action="sso.domains.delete", operation_id="sso_domains_delete", method="DELETE", path="/sso/domains/{id}", path_params=("id",), scope="sso:write", danger="destructive", approval=True, body=True, pagination=None), + "sso.domains.list": OperationSpec(action="sso.domains.list", operation_id="sso_domains_list", method="GET", path="/sso/domains", path_params=(), scope="sso:read", danger="read", approval=True, body=False, pagination="cursor"), + "sso.domains.verify": OperationSpec(action="sso.domains.verify", operation_id="sso_domains_verify", method="POST", path="/sso/domains/{id}/verify", path_params=("id",), scope="sso:write", danger="write", approval=True, body=True, pagination=None), + "sso.saml_metadata.import": OperationSpec(action="sso.saml_metadata.import", operation_id="sso_saml_metadata_import", method="POST", path="/sso/saml-metadata", path_params=(), scope="sso:write", danger="write", approval=True, body=True, pagination=None), + "support_sessions.end": OperationSpec(action="support_sessions.end", operation_id="support_sessions_end", method="DELETE", path="/support-sessions/{id}", path_params=("id",), scope="support:write", danger="destructive", approval=True, body=False, pagination=None), + "support_sessions.start": OperationSpec(action="support_sessions.start", operation_id="support_sessions_start", method="POST", path="/support-sessions", path_params=(), scope="support:write", danger=None, approval=True, body=True, pagination=None), + "token_vault.grants.create": OperationSpec(action="token_vault.grants.create", operation_id="token_vault_grants_create", method="POST", path="/token-vault/secrets/{id}/grants", path_params=("id",), scope="token_vault:write", danger="critical", approval=True, body=True, pagination=None), + "token_vault.grants.delete": OperationSpec(action="token_vault.grants.delete", operation_id="token_vault_grants_delete", method="DELETE", path="/token-vault/secrets/{id}/grants/{client_id}", path_params=("id", "client_id"), scope="token_vault:write", danger="destructive", approval=True, body=True, pagination=None), + "token_vault.secrets.create": OperationSpec(action="token_vault.secrets.create", operation_id="token_vault_secrets_create", method="POST", path="/token-vault/secrets", path_params=(), scope="token_vault:write", danger="write", approval=True, body=True, pagination=None), + "token_vault.secrets.get": OperationSpec(action="token_vault.secrets.get", operation_id="token_vault_secrets_get", method="GET", path="/token-vault/secrets/{id}", path_params=("id",), scope="token_vault:read", danger="read", approval=True, body=False, pagination=None), + "token_vault.secrets.list": OperationSpec(action="token_vault.secrets.list", operation_id="token_vault_secrets_list", method="GET", path="/token-vault/secrets", path_params=(), scope="token_vault:read", danger="read", approval=True, body=False, pagination="cursor"), + "token_vault.secrets.revoke": OperationSpec(action="token_vault.secrets.revoke", operation_id="token_vault_secrets_revoke", method="POST", path="/token-vault/secrets/{id}/revoke", path_params=("id",), scope="token_vault:write", danger="destructive", approval=True, body=True, pagination=None), + "token_vault.secrets.rotate": OperationSpec(action="token_vault.secrets.rotate", operation_id="token_vault_secrets_rotate", method="POST", path="/token-vault/secrets/{id}/rotate", path_params=("id",), scope="token_vault:write", danger="critical", approval=True, body=True, pagination=None), + "users.create": OperationSpec(action="users.create", operation_id="users_create", method="POST", path="/users", path_params=(), scope="users:write", danger=None, approval=True, body=True, pagination=None), + "users.deactivate": OperationSpec(action="users.deactivate", operation_id="users_deactivate", method="DELETE", path="/users/{id}", path_params=("id",), scope="users:write", danger=None, approval=True, body=False, pagination=None), + "users.environment_roles.get": OperationSpec(action="users.environment_roles.get", operation_id="users_environment_roles_get", method="GET", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:read", danger=None, approval=True, body=False, pagination=None), + "users.environment_roles.grant": OperationSpec(action="users.environment_roles.grant", operation_id="users_environment_roles_grant", method="PUT", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), + "users.environment_roles.list": OperationSpec(action="users.environment_roles.list", operation_id="users_environment_roles_list", method="GET", path="/users/{id}/environment-roles", path_params=("id",), scope="roles:read", danger=None, approval=True, body=False, pagination=None), + "users.environment_roles.revoke": OperationSpec(action="users.environment_roles.revoke", operation_id="users_environment_roles_revoke", method="DELETE", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), + "users.erase": OperationSpec(action="users.erase", operation_id="users_erase", method="POST", path="/users/{id}/erase", path_params=("id",), scope="users:erase", danger="critical", approval=True, body=False, pagination=None), + "users.get": OperationSpec(action="users.get", operation_id="users_get", method="GET", path="/users/{id}", path_params=("id",), scope="users:read", danger=None, approval=True, body=False, pagination=None), + "users.list": OperationSpec(action="users.list", operation_id="users_list", method="GET", path="/users", path_params=(), scope="users:read", danger=None, approval=True, body=False, pagination="cursor"), + "users.mfa.reset": OperationSpec(action="users.mfa.reset", operation_id="users_mfa_reset", method="DELETE", path="/users/{id}/mfa", path_params=("id",), scope="users:write", danger="critical", approval=True, body=False, pagination=None), + "users.password.set": OperationSpec(action="users.password.set", operation_id="users_password_set", method="POST", path="/users/{id}/password", path_params=("id",), scope="users:write", danger="critical", approval=True, body=True, pagination=None), + "users.password_reset.send": OperationSpec(action="users.password_reset.send", operation_id="users_password_reset_send", method="POST", path="/users/{id}/password-reset", path_params=("id",), scope="users:write", danger="write", approval=True, body=False, pagination=None), + "users.reactivate": OperationSpec(action="users.reactivate", operation_id="users_reactivate", method="POST", path="/users/{id}/reactivate", path_params=("id",), scope="users:write", danger="write", approval=True, body=False, pagination=None), + "users.sessions.list": OperationSpec(action="users.sessions.list", operation_id="users_sessions_list", method="GET", path="/users/{id}/sessions", path_params=("id",), scope="users:read", danger=None, approval=True, body=False, pagination=None), + "users.sessions.revoke": OperationSpec(action="users.sessions.revoke", operation_id="users_sessions_revoke", method="DELETE", path="/users/{id}/sessions/{session_id}", path_params=("id", "session_id"), scope="users:write", danger="destructive", approval=True, body=False, pagination=None), + "users.sessions.revoke_all": OperationSpec(action="users.sessions.revoke_all", operation_id="users_sessions_revoke_all", method="DELETE", path="/users/{id}/sessions", path_params=("id",), scope="users:write", danger="destructive", approval=True, body=False, pagination=None), + "users.update": OperationSpec(action="users.update", operation_id="users_update", method="PATCH", path="/users/{id}", path_params=("id",), scope="users:write", danger="critical", approval=True, body=True, pagination=None), + "users.verification.send": OperationSpec(action="users.verification.send", operation_id="users_verification_send", method="POST", path="/users/{id}/verification", path_params=("id",), scope="users:write", danger="write", approval=True, body=False, pagination=None), + "users.verify": OperationSpec(action="users.verify", operation_id="users_verify", method="POST", path="/users/{id}/verify", path_params=("id",), scope="users:write", danger="critical", approval=True, body=False, pagination=None), + "webhooks.create": OperationSpec(action="webhooks.create", operation_id="webhooks_create", method="POST", path="/webhooks", path_params=(), scope="webhooks:write", danger="critical", approval=True, body=True, pagination=None), + "webhooks.delete": OperationSpec(action="webhooks.delete", operation_id="webhooks_delete", method="DELETE", path="/webhooks/{id}", path_params=("id",), scope="webhooks:write", danger="destructive", approval=True, body=False, pagination=None), + "webhooks.get": OperationSpec(action="webhooks.get", operation_id="webhooks_get", method="GET", path="/webhooks/{id}", path_params=("id",), scope="webhooks:read", danger="read", approval=True, body=False, pagination=None), + "webhooks.list": OperationSpec(action="webhooks.list", operation_id="webhooks_list", method="GET", path="/webhooks", path_params=(), scope="webhooks:read", danger="read", approval=True, body=False, pagination="cursor"), + "webhooks.pause": OperationSpec(action="webhooks.pause", operation_id="webhooks_pause", method="POST", path="/webhooks/{id}/pause", path_params=("id",), scope="webhooks:write", danger="write", approval=True, body=False, pagination=None), + "webhooks.resume": OperationSpec(action="webhooks.resume", operation_id="webhooks_resume", method="POST", path="/webhooks/{id}/resume", path_params=("id",), scope="webhooks:write", danger="write", approval=True, body=False, pagination=None), + "webhooks.secret.rotate": OperationSpec(action="webhooks.secret.rotate", operation_id="webhooks_secret_rotate", method="POST", path="/webhooks/{id}/rotate", path_params=("id",), scope="webhooks:write", danger="critical", approval=True, body=False, pagination=None), + "webhooks.update": OperationSpec(action="webhooks.update", operation_id="webhooks_update", method="PATCH", path="/webhooks/{id}", path_params=("id",), scope="webhooks:write", danger="write", approval=True, body=True, pagination=None), +} + + +# ── Namespaces ──────────────────────────────────────────────────────────────────────────────────── + + +class AccessReviewsItemsMethods: + """``access_reviews.items.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def decide(self, id: str, item_id: str, body: AccessReviewsItemsDecideBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewItem]: ... + + @overload + def decide(self, id: str, item_id: str, body: AccessReviewsItemsDecideBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewItem] | PendingApprovalResult[AccessReviewItem]: ... + + def decide(self, id: str, item_id: str, body: AccessReviewsItemsDecideBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewItem] | PendingApprovalResult[AccessReviewItem]: + """Certify or revoke one item of an open access review. Revokes are applied when the review closes. + + Requires scope `governance:write`. Danger: write. + + ``POST /access-reviews/{id}/items/{item_id}`` · action ``access_reviews.items.decide`` + Scope ``governance:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["access_reviews.items.decide"], (id, item_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, id: str, query: AccessReviewsItemsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewsItemsListData]: ... + + @overload + def list(self, id: str, query: AccessReviewsItemsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewsItemsListData] | PendingApprovalResult[AccessReviewsItemsListData]: ... + + def list(self, id: str, query: AccessReviewsItemsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewsItemsListData] | PendingApprovalResult[AccessReviewsItemsListData]: + """List the items an access review asks someone to certify or revoke: who holds what, and the decision on each. + + Requires scope `governance:read`. Danger: read. + + ``GET /access-reviews/{id}/items`` · action ``access_reviews.items.list`` + Scope ``governance:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["access_reviews.items.list"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, id: str, query: AccessReviewsItemsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[AccessReviewItem]: + """Every item of ``access_reviews.items.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["access_reviews.items.list"], (id,), query, headers=headers) + + +class AccessReviewsMethods: + """``access_reviews.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.items = AccessReviewsItemsMethods(core) + + @overload + def close(self, id: str, body: AccessReviewsCloseBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview]: ... + + @overload + def close(self, id: str, body: AccessReviewsCloseBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview]: ... + + def close(self, id: str, body: AccessReviewsCloseBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview]: + """Close an access review and apply it: every revoke is carried out, and undecided items get the review's pending policy (revoke by default). + + Requires scope `governance:write`. Danger: destructive. + + ``POST /access-reviews/{id}/close`` · action ``access_reviews.close`` + Scope ``governance:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["access_reviews.close"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def create(self, body: AccessReviewsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview]: ... + + @overload + def create(self, body: AccessReviewsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview]: ... + + def create(self, body: AccessReviewsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview]: + """Open an access review of one organization's roles and memberships — or of staff roles — for someone to certify or revoke. + + Requires scope `governance:write`. Danger: write. + + ``POST /access-reviews`` · action ``access_reviews.create`` + Scope ``governance:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["access_reviews.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: AccessReviewsGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview]: ... + + @overload + def get(self, id: str, query: AccessReviewsGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview]: ... + + def get(self, id: str, query: AccessReviewsGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview]: + """Read one access review: its status, due date, pending-item policy and item count. + + Requires scope `governance:read`. Danger: read. + + ``GET /access-reviews/{id}`` · action ``access_reviews.get`` + Scope ``governance:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["access_reviews.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: AccessReviewsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewsListData]: ... + + @overload + def list(self, query: AccessReviewsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewsListData] | PendingApprovalResult[AccessReviewsListData]: ... + + def list(self, query: AccessReviewsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AccessReviewsListData] | PendingApprovalResult[AccessReviewsListData]: + """List access reviews (certification campaigns), open and closed, optionally those covering one organization. + + Requires scope `governance:read`. Danger: read. + + ``GET /access-reviews`` · action ``access_reviews.list`` + Scope ``governance:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["access_reviews.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: AccessReviewsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[AccessReview]: + """Every item of ``access_reviews.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["access_reviews.list"], (), query, headers=headers) + + +class ActionApprovalsMethods: + """``action_approvals.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + def get(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ActionApprovalsGetData]: + """Where an approval this key asked for stands + + Any key may poll the approvals it raised, and only those: another key's id is a 404. + `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + + ``GET /action-approvals/{id}`` + """ + return self._core.call(ENVIRONMENT_OPERATIONS["action_approvals.get"], (id,), None, headers=headers) + + +class ApiKeysMethods: + """``api_keys.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def list(self, organization_id: str, query: ApiKeysListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApiKeysListData]: ... + + @overload + def list(self, organization_id: str, query: ApiKeysListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApiKeysListData] | PendingApprovalResult[ApiKeysListData]: ... + + def list(self, organization_id: str, query: ApiKeysListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApiKeysListData] | PendingApprovalResult[ApiKeysListData]: + """List the API keys an organization's members created for your apps + + Requires scope `api_keys:read`. Revoked and expired keys are included, marked by + `status`. Never the key itself. Narrow to one app with `?client_id=`. + + ``GET /organizations/{organization_id}/api-keys`` · action ``api_keys.list`` + Scope ``api_keys:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["api_keys.list"], (organization_id,), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, organization_id: str, query: ApiKeysListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[ApiKey]: + """Every item of ``api_keys.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["api_keys.list"], (organization_id,), query, headers=headers) + + @overload + def revoke(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke a member API key + + Requires scope `api_keys:write`. The key stops verifying at once. Idempotent — an + already-revoked key is a 204 too. Recorded as `api_key.revoked`, with this + management key as the actor. + + ``DELETE /api-keys/{id}`` · action ``api_keys.revoke`` + Scope ``api_keys:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["api_keys.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ApisScopesMethods: + """``apis.scopes.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def define(self, id: str, key: str, body: ApisScopesDefineBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: ... + + @overload + def define(self, id: str, key: str, body: ApisScopesDefineBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: ... + + def define(self, id: str, key: str, body: ApisScopesDefineBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: + """Add a scope to an API, or change the description or tenant access of one it owns. + + Requires scope `apis:write`. Danger: write. + + ``PUT /apis/{id}/scopes/{key}`` · action ``apis.scopes.define`` + Scope ``apis:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.scopes.define"], (id, key), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def remove(self, id: str, key: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, id: str, key: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, id: str, key: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a scope from an API. Apps holding it keep it as a plain scope that no longer reaches the API. + + Requires scope `apis:write`. Danger: destructive. + + ``DELETE /apis/{id}/scopes/{key}`` · action ``apis.scopes.remove`` + Scope ``apis:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.scopes.remove"], (id, key), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ApisMethods: + """``apis.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.scopes = ApisScopesMethods(core) + + def create(self, body: ApisCreateBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: + """Register an API and its scopes + + Requires scope `apis:write`. Registering an API is the environment's act; no tenant + surface can. `identifier` is an absolute URI (RFC 8707) and becomes the token's + `aud`. Scope keys are unique across the environment. `client_id` links the app whose + roles and permissions the API enforces, and must have the API's owner. + `organization_id` makes the API one organization's; left out, the environment owns + it. Refusals are `422 invalid_api` with the reason. + + ``POST /apis`` + Scope ``apis:write``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.create"], (), body, idempotency_key=idempotency_key, headers=headers) + + def delete(self, id: str, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: + """Delete an API and its scopes + + Requires scope `apis:write`. Tokens already minted for it keep their `aud` until they + expire; apps holding its scope keys keep them as plain scopes. + + ``DELETE /apis/{id}`` + Scope ``apis:write``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.delete"], (id,), None, idempotency_key=idempotency_key, headers=headers) + + def get(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: + """Get an API + + Requires scope `apis:read`. + + ``GET /apis/{id}`` + Scope ``apis:read``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.get"], (id,), None, headers=headers) + + def list(self, query: ApisListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ApisListData]: + """List registered APIs + + Requires scope `apis:read`. + + ``GET /apis`` + Scope ``apis:read``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.list"], (), query, headers=headers) + + def list_all(self, query: ApisListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Api]: + """Every item of ``apis.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["apis.list"], (), query, headers=headers) + + def update(self, id: str, body: ApisUpdateBody | None = None, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: + """Change an API + + Requires scope `apis:write`. Any of: `name`; `client_id` (null unlinks the app); + `scopes`, which is the **complete** set afterwards — scopes named are added or + updated, scopes left out are removed. All or nothing. The identifier never changes: + register a new API instead. + + ``PATCH /apis/{id}`` + Scope ``apis:write``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apis.update"], (id,), body, idempotency_key=idempotency_key, headers=headers) + + +class ApprovalsMethods: + """``approvals.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def deny(self, request_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def deny(self, request_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def deny(self, request_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Deny a pending approval request (CIBA) for the person it was raised for: the agent gets access_denied and no token. + + Requires scope `approvals:write`. Danger: destructive. + + ``POST /agent-requests/{request_id}/deny`` · action ``approvals.deny`` + Scope ``approvals:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["approvals.deny"], (request_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: ApprovalsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApprovalsListData]: ... + + @overload + def list(self, query: ApprovalsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApprovalsListData] | PendingApprovalResult[ApprovalsListData]: ... + + def list(self, query: ApprovalsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApprovalsListData] | PendingApprovalResult[ApprovalsListData]: + """List the pending requests from agents to act as one of this environment's people (CIBA): which app, for whom, and what it asks. + + Requires scope `approvals:read`. Danger: read. + + ``GET /agent-requests`` · action ``approvals.list`` + Scope ``approvals:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["approvals.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: ApprovalsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[AgentRequest]: + """Every item of ``approvals.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["approvals.list"], (), query, headers=headers) + + +class AppsManifestMethods: + """``apps.manifest.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, id: str, body: AppsManifestSetBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def set(self, id: str, body: AppsManifestSetBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def set(self, id: str, body: AppsManifestSetBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Set or clear the URL an app publishes its roles-and-permissions manifest at. Does not fetch it; apps.manifest.sync does. + + Requires scope `apps:write`. Danger: write. + + ``PUT /apps/{id}/manifest`` · action ``apps.manifest.set`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.manifest.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def sync(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsManifestSyncData]: ... + + @overload + def sync(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsManifestSyncData] | PendingApprovalResult[AppsManifestSyncData]: ... + + def sync(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsManifestSyncData] | PendingApprovalResult[AppsManifestSyncData]: + """Fetch an app's published manifest now and sync the roles and permissions it declares. + + Requires scope `apps:write`. Danger: write. + + ``POST /apps/{id}/manifest/sync`` · action ``apps.manifest.sync`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.manifest.sync"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AppsScopesMethods: + """``apps.scopes.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, id: str, body: AppsScopesSetBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def set(self, id: str, body: AppsScopesSetBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def set(self, id: str, body: AppsScopesSetBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Replace an app's complete scope set — the ceiling of what it may request, applied from its next token. + + Requires scope `apps:write`. Danger: write. + + ``PUT /apps/{id}/scopes`` · action ``apps.scopes.set`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.scopes.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AppsSecretsMethods: + """``apps.secrets.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + def list(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsSecretsListData]: + """List an app's live client secrets + + Requires scope `apps:read`. Every live secret, newest first: its id (what + `DELETE /apps/{id}/secrets/{secret_id}` names), the characters it ends in, and its + dates — never a secret. `expires_at` is set on one a rotation replaced, still working + through its overlap. An app holds a handful at most, so there is no page to turn. + + ``GET /apps/{id}/secrets`` · action ``apps.secrets.list`` + Scope ``apps:read``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.secrets.list"], (id,), None, headers=headers) + + @overload + def revoke(self, id: str, secret_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, secret_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, secret_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke one of an app's client secrets immediately. Never its last live secret — rotate that instead. + + Requires scope `apps:write`. Danger: critical. + + ``DELETE /apps/{id}/secrets/{secret_id}`` · action ``apps.secrets.revoke`` + Scope ``apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.secrets.revoke"], (id, secret_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def rotate(self, id: str, body: AppsSecretsRotateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppSecret]: ... + + @overload + def rotate(self, id: str, body: AppsSecretsRotateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppSecret] | PendingApprovalResult[AppSecret]: ... + + def rotate(self, id: str, body: AppsSecretsRotateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppSecret] | PendingApprovalResult[AppSecret]: + """Mint a new client secret for an app and retire the current ones after grace_seconds (0 = at once). The new secret is returned once. + + Requires scope `apps:write`. Danger: critical. + + ``POST /apps/{id}/secrets`` · action ``apps.secrets.rotate`` + Scope ``apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.secrets.rotate"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AppsSettingsMethods: + """``apps.settings.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def api_key_prefix(self, id: str, body: AppsSettingsApiKeyPrefixBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def api_key_prefix(self, id: str, body: AppsSettingsApiKeyPrefixBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def api_key_prefix(self, id: str, body: AppsSettingsApiKeyPrefixBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Set the prefix of an app's customer API keys (acme_live), which lets its users create keys for it, or null to stop new keys. + + Requires scope `apps:write`. Danger: write. + + ``PUT /apps/{id}/settings/api-key-prefix`` · action ``apps.settings.api_key_prefix`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.settings.api_key_prefix"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def backchannel_logout(self, id: str, body: AppsSettingsBackchannelLogoutBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def backchannel_logout(self, id: str, body: AppsSettingsBackchannelLogoutBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def backchannel_logout(self, id: str, body: AppsSettingsBackchannelLogoutBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Set or clear the URI an app is sent a logout token at when somebody signs out (OIDC Back-Channel Logout). + + Requires scope `apps:write`. Danger: write. + + ``PUT /apps/{id}/settings/backchannel-logout`` · action ``apps.settings.backchannel_logout`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.settings.backchannel_logout"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def token_exchange(self, id: str, body: AppsSettingsTokenExchangeBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def token_exchange(self, id: str, body: AppsSettingsTokenExchangeBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def token_exchange(self, id: str, body: AppsSettingsTokenExchangeBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Turn token exchange (RFC 8693) on or off for a confidential app. + + Requires scope `apps:write`. Danger: write. + + ``PUT /apps/{id}/settings/token-exchange`` · action ``apps.settings.token_exchange`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.settings.token_exchange"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def token_lifetime(self, id: str, body: AppsSettingsTokenLifetimeBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def token_lifetime(self, id: str, body: AppsSettingsTokenLifetimeBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def token_lifetime(self, id: str, body: AppsSettingsTokenLifetimeBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Set how long an app's access tokens live, in seconds, or null for the install's default. + + Requires scope `apps:write`. Danger: write. + + ``PUT /apps/{id}/settings/token-lifetime`` · action ``apps.settings.token_lifetime`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.settings.token_lifetime"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AppsMethods: + """``apps.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.manifest = AppsManifestMethods(core) + self.scopes = AppsScopesMethods(core) + self.secrets = AppsSecretsMethods(core) + self.settings = AppsSettingsMethods(core) + + def blueprint(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[AppBlueprint]: + """Export an app's configuration as a blueprint + + Requires scope `apps:read`. `{id}` is the app's id or its `client_id`. The document + carries the app's settings and none of its identity or credentials: no client id, + no secret, no key set, no owning organization. `POST /apps` with it as `blueprint` + creates the same app in another environment. + + ``GET /apps/{id}/blueprint`` · action ``apps.blueprint`` + Scope ``apps:read``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.blueprint"], (id,), None, headers=headers) + + @overload + def copy(self, id: str, body: AppsCopyBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def copy(self, id: str, body: AppsCopyBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def copy(self, id: str, body: AppsCopyBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Copy an app into another environment of this project, with a new client id and secret there. A person on the environment console only; a key exports the blueprint instead. + + Requires scope `apps:write`. Danger: critical. + + ``POST /apps/{id}/copy`` · action ``apps.copy`` + Scope ``apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.copy"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + def create(self, body: AppsCreateBody | None = None, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: + """Register an app + + Requires scope `apps:write`. Two ways to describe it: + + - **From a blueprint** — send `GET /apps/{id}/blueprint`'s document from another + environment as `blueprint` (promote staging to production). `name`, + `redirect_uris` and `post_logout_redirect_uris` beside it replace the + blueprint's own, since those usually name the environment it came from. A + `private_key_jwt` blueprint needs `jwks`. + - **Short form** — `name` and `type` (`web`, `spa`, `cli`, `service`, `agent`; default + `web`), which decide the client type, grants and default scopes as the console's + "Create app" does. `advanced` takes `client_type` and `grant_types`. + + The response carries `client_secret` for an app that authenticates with one — + **once**. Only its hash is kept, and an idempotent replay returns `client_secret: null`. + + A platform scope reserved for the console (`vault.manage`, `decisions:read`) is + refused (`scope_not_grantable`), as is a registered API's scope this app's owner may + not hold. Danger: critical — it mints a credential. + + ``POST /apps`` · action ``apps.create`` + Scope ``apps:write`` · danger: critical. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.create"], (), body, idempotency_key=idempotency_key, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete an app and every secret it holds. Anything signing in as it stops working immediately. + + Requires scope `apps:write`. Danger: critical. + + ``DELETE /apps/{id}`` · action ``apps.delete`` + Scope ``apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Get one app (OAuth client) by its id or its client_id: its kind, grants, redirect URIs, scopes and settings. Never a secret. + + Requires scope `apps:read`. Danger: read. + + ``GET /apps/{id}`` · action ``apps.get`` + Scope ``apps:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + def list(self, query: AppsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsListData]: + """List apps + + Requires scope `apps:read`. Never a secret. + + ``GET /apps`` · action ``apps.list`` + Scope ``apps:read``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.list"], (), query, headers=headers) + + def list_all(self, query: AppsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[App]: + """Every item of ``apps.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["apps.list"], (), query, headers=headers) + + @overload + def update(self, id: str, body: AppsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def update(self, id: str, body: AppsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def update(self, id: str, body: AppsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: + """Rename an app and replace its redirect URIs or sign-out redirect URIs. Fields left out are unchanged. + + Requires scope `apps:write`. Danger: write. + + ``PATCH /apps/{id}`` · action ``apps.update`` + Scope ``apps:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["apps.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AuditMethods: + """``audit.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def list(self, query: AuditListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditListData]: ... + + @overload + def list(self, query: AuditListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditListData] | PendingApprovalResult[AuditListData]: ... + + def list(self, query: AuditListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditListData] | PendingApprovalResult[AuditListData]: + """Read this environment's audit trail oldest first, optionally narrowed by action, actor type or organization; pass the last id as `after` to page. + + Requires scope `audit:read`. Danger: read. + + ``GET /audit-log`` · action ``audit.list`` + Scope ``audit:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: AuditListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[AuditEntry]: + """Every item of ``audit.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["audit.list"], (), query, headers=headers) + + +class AuditLogsEventsMethods: + """``audit_logs.events.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: AuditLogsEventsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogEventBatch]: ... + + @overload + def create(self, body: AuditLogsEventsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogEventBatch] | PendingApprovalResult[AuditLogEventBatch]: ... + + def create(self, body: AuditLogsEventsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogEventBatch] | PendingApprovalResult[AuditLogEventBatch]: + """Record 1–100 audit events your app's users caused, each for one of your organizations (customers); checked against the action's schema when it has one and appended to that organization's tamper-evident chain. Send an Idempotency-Key. + + Requires scope `audit_logs:write`. Danger: write. + + ``POST /audit-logs/events`` · action ``audit_logs.events.create`` + Scope ``audit_logs:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.events.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: AuditLogsEventsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogsEventsListData]: ... + + @overload + def list(self, query: AuditLogsEventsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogsEventsListData] | PendingApprovalResult[AuditLogsEventsListData]: ... + + def list(self, query: AuditLogsEventsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogsEventsListData] | PendingApprovalResult[AuditLogsEventsListData]: + """Read audit events newest first, for one organization or the whole environment, filtered by action, actor, target and time range; pass `next_cursor` as `after` to page. + + Requires scope `audit_logs:read`. Danger: read. + + ``GET /audit-logs/events`` · action ``audit_logs.events.list`` + Scope ``audit_logs:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.events.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: AuditLogsEventsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[AuditLogEvent]: + """Every item of ``audit_logs.events.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["audit_logs.events.list"], (), query, headers=headers) + + +class AuditLogsExportsMethods: + """``audit_logs.exports.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: AuditLogsExportsCreateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogExport]: ... + + @overload + def create(self, body: AuditLogsExportsCreateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogExport] | PendingApprovalResult[AuditLogExport]: ... + + def create(self, body: AuditLogsExportsCreateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogExport] | PendingApprovalResult[AuditLogExport]: + """Start a CSV export of audit events (same filters as the list). It is written on the queue: poll audit_logs.exports.get until state is ready, then download from its short-lived url. + + Requires scope `audit_logs:export`. Danger: write. + + ``POST /audit-logs/exports`` · action ``audit_logs.exports.create`` + Scope ``audit_logs:export`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.exports.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogExport]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogExport] | PendingApprovalResult[AuditLogExport]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogExport] | PendingApprovalResult[AuditLogExport]: + """Read an audit-log export's state; once ready it carries a signed download url valid for a few minutes (read it again for a fresh one). + + Requires scope `audit_logs:read`. Danger: read. + + ``GET /audit-logs/exports/{id}`` · action ``audit_logs.exports.get`` + Scope ``audit_logs:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.exports.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + +class AuditLogsSchemasMethods: + """``audit_logs.schemas.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: AuditLogsSchemasCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema]: ... + + @overload + def create(self, body: AuditLogsSchemasCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema]: ... + + def create(self, body: AuditLogsSchemasCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema]: + """Define the schema one audit-log action's events must match — allowed target types and metadata schemas (a subset of JSON Schema). Events of that action are then validated on arrival. + + Requires scope `audit_logs:manage`. Danger: write. + + ``POST /audit-logs/schemas`` · action ``audit_logs.schemas.create`` + Scope ``audit_logs:manage`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.schemas.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, action: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, action: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, action: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete an audit-log action's schema. Its events are then accepted unchecked — or refused, if the environment is in strict mode. + + Requires scope `audit_logs:manage`. Danger: destructive. + + ``DELETE /audit-logs/schemas/{action}`` · action ``audit_logs.schemas.delete`` + Scope ``audit_logs:manage`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.schemas.delete"], (action,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, action: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema]: ... + + @overload + def get(self, action: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema]: ... + + def get(self, action: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema]: + """Read the schema one audit-log action's events are validated against. + + Requires scope `audit_logs:read`. Danger: read. + + ``GET /audit-logs/schemas/{action}`` · action ``audit_logs.schemas.get`` + Scope ``audit_logs:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.schemas.get"], (action,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: AuditLogsSchemasListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogsSchemasListData]: ... + + @overload + def list(self, query: AuditLogsSchemasListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogsSchemasListData] | PendingApprovalResult[AuditLogsSchemasListData]: ... + + def list(self, query: AuditLogsSchemasListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogsSchemasListData] | PendingApprovalResult[AuditLogsSchemasListData]: + """List the audit-log schemas this environment validates events against, one per action. + + Requires scope `audit_logs:read`. Danger: read. + + ``GET /audit-logs/schemas`` · action ``audit_logs.schemas.list`` + Scope ``audit_logs:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.schemas.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: AuditLogsSchemasListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[AuditLogSchema]: + """Every item of ``audit_logs.schemas.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["audit_logs.schemas.list"], (), query, headers=headers) + + @overload + def update(self, action: str, body: AuditLogsSchemasUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema]: ... + + @overload + def update(self, action: str, body: AuditLogsSchemasUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema]: ... + + def update(self, action: str, body: AuditLogsSchemasUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema]: + """Replace an audit-log action's schema with a new version (omitted parts are removed). Recorded events keep the version they were checked against. + + Requires scope `audit_logs:manage`. Danger: write. + + ``PUT /audit-logs/schemas/{action}`` · action ``audit_logs.schemas.update`` + Scope ``audit_logs:manage`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.schemas.update"], (action,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AuditLogsSettingsMethods: + """``audit_logs.settings.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def get(self, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSettings]: ... + + @overload + def get(self, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSettings] | PendingApprovalResult[AuditLogSettings]: ... + + def get(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSettings] | PendingApprovalResult[AuditLogSettings]: + """Read this environment's audit-log settings: retention in days and whether strict schemas are on. + + Requires scope `audit_logs:read`. Danger: read. + + ``GET /audit-logs/settings`` · action ``audit_logs.settings.get`` + Scope ``audit_logs:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.settings.get"], (), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def update(self, body: AuditLogsSettingsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSettings]: ... + + @overload + def update(self, body: AuditLogsSettingsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSettings] | PendingApprovalResult[AuditLogSettings]: ... + + def update(self, body: AuditLogsSettingsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogSettings] | PendingApprovalResult[AuditLogSettings]: + """Change this environment's audit-log retention (days, 1–3650) and strict mode. Shortening retention deletes older events at the next daily prune — irreversibly. + + Requires scope `audit_logs:manage`. Danger: destructive. + + ``PATCH /audit-logs/settings`` · action ``audit_logs.settings.update`` + Scope ``audit_logs:manage`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.settings.update"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class AuditLogsMethods: + """``audit_logs.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.events = AuditLogsEventsMethods(core) + self.exports = AuditLogsExportsMethods(core) + self.schemas = AuditLogsSchemasMethods(core) + self.settings = AuditLogsSettingsMethods(core) + + @overload + def verify(self, query: AuditLogsVerifyQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogVerification]: ... + + @overload + def verify(self, query: AuditLogsVerifyQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogVerification] | PendingApprovalResult[AuditLogVerification]: ... + + def verify(self, query: AuditLogsVerifyQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AuditLogVerification] | PendingApprovalResult[AuditLogVerification]: + """Re-hash one organization's audit-event chain (from the oldest event retention kept, or from_sequence) and report whether every event is unchanged and in place. + + Requires scope `audit_logs:read`. Danger: read. + + ``GET /audit-logs/verify`` · action ``audit_logs.verify`` + Scope ``audit_logs:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["audit_logs.verify"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + +class BrandingAppearanceMethods: + """``branding.appearance.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def get(self, query: BrandingAppearanceGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Appearance]: ... + + @overload + def get(self, query: BrandingAppearanceGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Appearance] | PendingApprovalResult[Appearance]: ... + + def get(self, query: BrandingAppearanceGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Appearance] | PendingApprovalResult[Appearance]: + """Read the hosted sign-in theme: the environment default, or one organization's own. + + Requires scope `branding:read`. Danger: read. + + ``GET /branding/appearance`` · action ``branding.appearance.get`` + Scope ``branding:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["branding.appearance.get"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def set(self, body: BrandingAppearanceSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Appearance]: ... + + @overload + def set(self, body: BrandingAppearanceSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Appearance] | PendingApprovalResult[Appearance]: ... + + def set(self, body: BrandingAppearanceSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Appearance] | PendingApprovalResult[Appearance]: + """Set the hosted sign-in theme (preset, colours, corners, type, logo) for the environment default or one organization. + + Requires scope `branding:write`. Danger: write. + + ``PUT /branding/appearance`` · action ``branding.appearance.set`` + Scope ``branding:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["branding.appearance.set"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class BrandingWhitelabelMethods: + """``branding.whitelabel.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def get(self, query: BrandingWhitelabelGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WhitelabelBranding]: ... + + @overload + def get(self, query: BrandingWhitelabelGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WhitelabelBranding] | PendingApprovalResult[WhitelabelBranding]: ... + + def get(self, query: BrandingWhitelabelGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WhitelabelBranding] | PendingApprovalResult[WhitelabelBranding]: + """Read the white-label branding (palette, app name, email sender, logo) of the environment default or one organization. + + Requires scope `branding:read`. Danger: read. + + ``GET /branding/whitelabel`` · action ``branding.whitelabel.get`` + Scope ``branding:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["branding.whitelabel.get"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def set(self, body: BrandingWhitelabelSetBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WhitelabelBranding]: ... + + @overload + def set(self, body: BrandingWhitelabelSetBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WhitelabelBranding] | PendingApprovalResult[WhitelabelBranding]: ... + + def set(self, body: BrandingWhitelabelSetBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WhitelabelBranding] | PendingApprovalResult[WhitelabelBranding]: + """Save the white-label branding (palette, app name, email sender, welcome email) of the environment default or one organization. + + Requires scope `branding:write`. Danger: write. + + ``PUT /branding/whitelabel`` · action ``branding.whitelabel.set`` + Scope ``branding:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["branding.whitelabel.set"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class BrandingMethods: + """``branding.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.appearance = BrandingAppearanceMethods(core) + self.whitelabel = BrandingWhitelabelMethods(core) + + +class DirectoriesGroupsMethods: + """``directories.groups.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def list(self, id: str, query: DirectoriesGroupsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoriesGroupsListData]: ... + + @overload + def list(self, id: str, query: DirectoriesGroupsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoriesGroupsListData] | PendingApprovalResult[DirectoriesGroupsListData]: ... + + def list(self, id: str, query: DirectoriesGroupsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoriesGroupsListData] | PendingApprovalResult[DirectoriesGroupsListData]: + """List the groups a directory has synced, with the role ids each group is mapped onto. + + Requires scope `directory_sync:read`. Danger: read. + + ``GET /directories/{id}/groups`` · action ``directories.groups.list`` + Scope ``directory_sync:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.groups.list"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, id: str, query: DirectoriesGroupsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[DirectoryGroup]: + """Every item of ``directories.groups.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["directories.groups.list"], (id,), query, headers=headers) + + @overload + def map(self, id: str, body: DirectoriesGroupsMapBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoryGroup]: ... + + @overload + def map(self, id: str, body: DirectoriesGroupsMapBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoryGroup] | PendingApprovalResult[DirectoryGroup]: ... + + def map(self, id: str, body: DirectoriesGroupsMapBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoryGroup] | PendingApprovalResult[DirectoryGroup]: + """Map a directory group onto a role (everyone in the group holds it as membership syncs), or unmap it. + + Requires scope `directory_sync:write`. Danger: write. + + ``POST /directories/{id}/group-roles`` · action ``directories.groups.map`` + Scope ``directory_sync:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.groups.map"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class DirectoriesStatusMethods: + """``directories.status.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, id: str, body: DirectoriesStatusSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory]: ... + + @overload + def set(self, id: str, body: DirectoriesStatusSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: ... + + def set(self, id: str, body: DirectoriesStatusSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: + """Pause or resume an inbound directory's provisioning. + + Requires scope `directory_sync:write`. Danger: write. + + ``POST /directories/{id}/status`` · action ``directories.status.set`` + Scope ``directory_sync:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.status.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class DirectoriesTokenMethods: + """``directories.token.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def rotate(self, id: str, body: DirectoriesTokenRotateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory]: ... + + @overload + def rotate(self, id: str, body: DirectoriesTokenRotateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: ... + + def rotate(self, id: str, body: DirectoriesTokenRotateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: + """Issue a new bearer token for a SCIM directory, returned once. The old token stops working immediately. + + Requires scope `directory_sync:write`. Danger: critical. + + ``POST /directories/{id}/rotate`` · action ``directories.token.rotate`` + Scope ``directory_sync:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.token.rotate"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class DirectoriesMethods: + """``directories.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.groups = DirectoriesGroupsMethods(core) + self.status = DirectoriesStatusMethods(core) + self.token = DirectoriesTokenMethods(core) + + @overload + def connect(self, body: DirectoriesConnectBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory]: ... + + @overload + def connect(self, body: DirectoriesConnectBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: ... + + def connect(self, body: DirectoriesConnectBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: + """Connect a Google Workspace or Microsoft Entra directory to sync an organization's people from, verifying the credentials first. Runs the first sync. + + Requires scope `directory_sync:write`. Danger: critical. + + ``POST /directories/connect`` · action ``directories.connect`` + Scope ``directory_sync:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.connect"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def create(self, body: DirectoriesCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory]: ... + + @overload + def create(self, body: DirectoriesCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: ... + + def create(self, body: DirectoriesCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: + """Register a SCIM directory for an organization. Answers the SCIM base URL and a bearer token, shown once. + + Requires scope `directory_sync:write`. Danger: critical. + + ``POST /directories`` · action ``directories.create`` + Scope ``directory_sync:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, body: DirectoriesDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, body: DirectoriesDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, body: DirectoriesDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete an inbound directory. Its token stops working; the people it provisioned keep their accounts. + + Requires scope `directory_sync:write`. Danger: destructive. + + ``DELETE /directories/{id}`` · action ``directories.delete`` + Scope ``directory_sync:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.delete"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: DirectoriesGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory]: ... + + @overload + def get(self, id: str, query: DirectoriesGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: ... + + def get(self, id: str, query: DirectoriesGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: + """Read one inbound directory: its provider, status, SCIM base URL and last sync error. Never its token or credentials. + + Requires scope `directory_sync:read`. Danger: read. + + ``GET /directories/{id}`` · action ``directories.get`` + Scope ``directory_sync:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: DirectoriesListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoriesListData]: ... + + @overload + def list(self, query: DirectoriesListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoriesListData] | PendingApprovalResult[DirectoriesListData]: ... + + def list(self, query: DirectoriesListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[DirectoriesListData] | PendingApprovalResult[DirectoriesListData]: + """List the directories (SCIM, Google Workspace, Microsoft Entra) syncing people in, optionally for one organization, with their last sync error. + + Requires scope `directory_sync:read`. Danger: read. + + ``GET /directories`` · action ``directories.list`` + Scope ``directory_sync:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: DirectoriesListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Directory]: + """Every item of ``directories.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["directories.list"], (), query, headers=headers) + + @overload + def update(self, id: str, body: DirectoriesUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory]: ... + + @overload + def update(self, id: str, body: DirectoriesUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: ... + + def update(self, id: str, body: DirectoriesUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Directory] | PendingApprovalResult[Directory]: + """Rename an inbound directory. + + Requires scope `directory_sync:write`. Danger: write. + + ``PATCH /directories/{id}`` · action ``directories.update`` + Scope ``directory_sync:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["directories.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class DomainsMethods: + """``domains.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def add(self, body: DomainsAddBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain]: ... + + @overload + def add(self, body: DomainsAddBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: ... + + def add(self, body: DomainsAddBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: + """Start serving this environment on a custom domain: returns the DNS TXT record that proves you control it. + + Requires scope `domains:write`. Danger: write. + + ``POST /domains`` · action ``domains.add`` + Scope ``domains:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["domains.add"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain]: ... + + @overload + def get(self, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: ... + + def get(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: + """Read this environment's custom domain, and the DNS TXT record that proves a pending one. + + Requires scope `domains:read`. Danger: read. + + ``GET /domains`` · action ``domains.get`` + Scope ``domains:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["domains.get"], (), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def remove(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Stop serving this environment on its custom domain. Anything using that domain stops reaching the environment. + + Requires scope `domains:write`. Danger: destructive. + + ``DELETE /domains`` · action ``domains.remove`` + Scope ``domains:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["domains.remove"], (), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def verify(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain]: ... + + @overload + def verify(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: ... + + def verify(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: + """Look for the pending domain's DNS TXT record and, once it is visible, serve this environment on that domain. + + Requires scope `domains:write`. Danger: write. + + ``POST /domains/verify`` · action ``domains.verify`` + Scope ``domains:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["domains.verify"], (), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class EventsMethods: + """``events.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def list(self, query: EventsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EventsListData]: ... + + @overload + def list(self, query: EventsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EventsListData] | PendingApprovalResult[EventsListData]: ... + + def list(self, query: EventsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EventsListData] | PendingApprovalResult[EventsListData]: + """Read this environment's domain events (the facts webhooks deliver) oldest first; pass the last id as `after` to poll for new ones. + + Requires scope `events:read`. Danger: read. + + ``GET /events`` · action ``events.list`` + Scope ``events:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["events.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: EventsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[DomainEvent]: + """Every item of ``events.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["events.list"], (), query, headers=headers) + + +class FrontendKeysMethods: + """``frontend_keys.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: FrontendKeysCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKey]: ... + + @overload + def create(self, body: FrontendKeysCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKey] | PendingApprovalResult[FrontendKey]: ... + + def create(self, body: FrontendKeysCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKey] | PendingApprovalResult[FrontendKey]: + """Create a publishable frontend key for a browser app, usable only from the origins listed. The key is public by design. + + Requires scope `frontend_keys:write`. Danger: write. + + ``POST /frontend-keys`` · action ``frontend_keys.create`` + Scope ``frontend_keys:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["frontend_keys.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: FrontendKeysListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKeysListData]: ... + + @overload + def list(self, query: FrontendKeysListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKeysListData] | PendingApprovalResult[FrontendKeysListData]: ... + + def list(self, query: FrontendKeysListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKeysListData] | PendingApprovalResult[FrontendKeysListData]: + """List the publishable frontend keys browser apps present to the Frontend API, in full, with their allowed origins. + + Requires scope `frontend_keys:read`. Danger: read. + + ``GET /frontend-keys`` · action ``frontend_keys.list`` + Scope ``frontend_keys:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["frontend_keys.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: FrontendKeysListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[FrontendKey]: + """Every item of ``frontend_keys.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["frontend_keys.list"], (), query, headers=headers) + + @overload + def revoke(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke a publishable frontend key. Pages still holding it stop working immediately. + + Requires scope `frontend_keys:write`. Danger: destructive. + + ``DELETE /frontend-keys/{id}`` · action ``frontend_keys.revoke`` + Scope ``frontend_keys:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["frontend_keys.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def set_origins(self, id: str, body: FrontendKeysSetOriginsBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKey]: ... + + @overload + def set_origins(self, id: str, body: FrontendKeysSetOriginsBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKey] | PendingApprovalResult[FrontendKey]: ... + + def set_origins(self, id: str, body: FrontendKeysSetOriginsBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[FrontendKey] | PendingApprovalResult[FrontendKey]: + """Replace the exact list of origins allowed to present a publishable frontend key. Live on the next request. + + Requires scope `frontend_keys:write`. Danger: write. + + ``PUT /frontend-keys/{id}/origins`` · action ``frontend_keys.set_origins`` + Scope ``frontend_keys:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["frontend_keys.set_origins"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class HooksMethods: + """``hooks.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: HooksCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook]: ... + + @overload + def create(self, body: HooksCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook]: ... + + def create(self, body: HooksCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook]: + """Register a hook at a hook point (token minting, login, registration, password change). Returns its signing secret once. + + Requires scope `hooks:write`. Danger: critical. + + ``POST /hooks`` · action ``hooks.create`` + Scope ``hooks:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["hooks.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a hook. It is no longer called at its hook point. + + Requires scope `hooks:write`. Danger: destructive. + + ``DELETE /hooks/{id}`` · action ``hooks.delete`` + Scope ``hooks:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["hooks.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook]: + """Get one hook: its URL, hook point, owner and whether it is active. Never its signing secret. + + Requires scope `hooks:read`. Danger: read. + + ``GET /hooks/{id}`` · action ``hooks.get`` + Scope ``hooks:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["hooks.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: HooksListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[HooksListData]: ... + + @overload + def list(self, query: HooksListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[HooksListData] | PendingApprovalResult[HooksListData]: ... + + def list(self, query: HooksListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[HooksListData] | PendingApprovalResult[HooksListData]: + """List the hooks — endpoints called during sign-in and token issuance — with their hook point, owner and whether each is active. + + Requires scope `hooks:read`. Danger: read. + + ``GET /hooks`` · action ``hooks.list`` + Scope ``hooks:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["hooks.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: HooksListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[InlineHook]: + """Every item of ``hooks.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["hooks.list"], (), query, headers=headers) + + @overload + def update(self, id: str, body: HooksUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook]: ... + + @overload + def update(self, id: str, body: HooksUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook]: ... + + def update(self, id: str, body: HooksUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook]: + """Pause (active: false) or activate (active: true) a hook. + + Requires scope `hooks:write`. Danger: critical. + + ``PATCH /hooks/{id}`` · action ``hooks.update`` + Scope ``hooks:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["hooks.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class InvitationsMethods: + """``invitations.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def list(self, organization_id: str, query: InvitationsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InvitationsListData]: ... + + @overload + def list(self, organization_id: str, query: InvitationsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InvitationsListData] | PendingApprovalResult[InvitationsListData]: ... + + def list(self, organization_id: str, query: InvitationsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[InvitationsListData] | PendingApprovalResult[InvitationsListData]: + """List an organization's pending invitations + + Requires scope `invitations:read`. Pending and unexpired only. + + ``GET /organizations/{organization_id}/invitations`` · action ``invitations.list`` + Scope ``invitations:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["invitations.list"], (organization_id,), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, organization_id: str, query: InvitationsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Invitation]: + """Every item of ``invitations.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["invitations.list"], (organization_id,), query, headers=headers) + + @overload + def resend(self, organization_id: str, invitation_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Invitation]: ... + + @overload + def resend(self, organization_id: str, invitation_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation]: ... + + def resend(self, organization_id: str, invitation_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation]: + """Mail a pending invitation again + + Requires scope `invitations:write`. On a **fresh link**: only a hash of the old one is + kept, so it cannot be re-sent, and the reason people ask is usually that it expired. + The response is the NEW invitation — its `id` replaces the old one, which stops + working. Its roles and app context move with it. + + One re-send per address per minute (`429 too_soon`); `409 not_pending` for one that + was accepted, withdrawn or has expired; `503 mail_failed` keeps the invitation. + + ``POST /organizations/{organization_id}/invitations/{invitation_id}/resend`` · action ``invitations.resend`` + Scope ``invitations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["invitations.resend"], (organization_id, invitation_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, organization_id: str, invitation_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, organization_id: str, invitation_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, organization_id: str, invitation_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Withdraw a pending invitation + + Requires scope `invitations:write`. The link stops working, and the roles parked + for it go with it. `409 not_pending` for one already accepted, withdrawn or expired. + + ``DELETE /organizations/{organization_id}/invitations/{invitation_id}`` · action ``invitations.revoke`` + Scope ``invitations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["invitations.revoke"], (organization_id, invitation_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def send(self, organization_id: str, body: InvitationsSendBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Invitation]: ... + + @overload + def send(self, organization_id: str, body: InvitationsSendBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation]: ... + + def send(self, organization_id: str, body: InvitationsSendBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation]: + """Invite someone to the organization + + Requires scope `invitations:write`. Mails a link; the person joins when they open it + and confirm. Inviting an address again replaces its earlier pending invitation. + + - `role` — `admin` or `member` (default `member`). Never `owner`. + - `roles` — access roles granted when the invitation is accepted: role ids, or your + app's manifest **keys** when `client_id` names the app that declared them. Only + roles the organization's own administrators could grant are accepted: a **staff + role** is refused (`422 role_not_assignable`) — grant it after they join, with + `PUT …/members/{user_id}/roles/{role_id}`. + - `client_id` + `return_to` — after accepting, the person is sent to `return_to`, + which must be on one of that app's registered redirect-URI origins (checked again + when the invitation is accepted). + - `inviter_name` — who the mail says it is from. Defaults to the app's name, or the + environment's. + + Refusals: `409 already_member`, `409 access_role_conflict` (segregation of duties), + `422 unknown_role`, `422 role_not_assignable`, `422 unknown_app`, + `422 return_without_app`, `422 return_to_malformed`, `422 return_to_not_registered`, + `503 mail_failed` (nothing was created — retry). + + ``POST /organizations/{organization_id}/invitations`` · action ``invitations.send`` + Scope ``invitations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["invitations.send"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class KeysMethods: + """``keys.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: KeysCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysCreateData]: ... + + @overload + def create(self, body: KeysCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysCreateData] | PendingApprovalResult[KeysCreateData]: ... + + def create(self, body: KeysCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysCreateData] | PendingApprovalResult[KeysCreateData]: + """Mint a management key for this environment, at most as wide as the caller. The value is returned once, as `token`. + + Requires scope `keys:write`. Danger: critical. + + ``POST /keys`` · action ``keys.create`` + Scope ``keys:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["keys.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: KeysListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysListData]: ... + + @overload + def list(self, query: KeysListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysListData] | PendingApprovalResult[KeysListData]: ... + + def list(self, query: KeysListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysListData] | PendingApprovalResult[KeysListData]: + """List this environment's management keys (names, scopes, parents, expiry, last use — never their values). + + Requires scope `keys:read`. Danger: read. + + ``GET /keys`` · action ``keys.list`` + Scope ``keys:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["keys.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: KeysListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[KeysListItem]: + """Every item of ``keys.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["keys.list"], (), query, headers=headers) + + @overload + def revoke(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke a management key and every key it minted, immediately. + + Requires scope `keys:write`. Danger: destructive. + + ``DELETE /keys/{id}`` · action ``keys.revoke`` + Scope ``keys:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["keys.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysRotateData]: ... + + @overload + def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysRotateData] | PendingApprovalResult[KeysRotateData]: ... + + def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysRotateData] | PendingApprovalResult[KeysRotateData]: + """Rotate a management key: mint a successor with the same scopes and retire the old one after a grace period. + + Requires scope `keys:write`. Danger: critical. + + ``POST /keys/{id}/rotate`` · action ``keys.rotate`` + Scope ``keys:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["keys.rotate"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class LegacyLoginMethods: + """``legacy_login.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def approve(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin]: ... + + @overload + def approve(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin]: ... + + def approve(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin]: + """Approve the declared legacy login endpoint: sign-ins for people not yet migrated, with their passwords, go to that URL. + + Requires scope `signin:write`. Danger: critical. + + ``POST /legacy-login/approve`` · action ``legacy_login.approve`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["legacy_login.approve"], (), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin]: ... + + @overload + def get(self, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin]: ... + + def get(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin]: + """Read the legacy login endpoint an app declared for migration, and whether it is approved to receive sign-ins. + + Requires scope `signin:read`. Danger: read. + + ``GET /legacy-login`` · action ``legacy_login.get`` + Scope ``signin:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["legacy_login.get"], (), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def probe(self, body: LegacyLoginProbeBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbeData]: ... + + @overload + def probe(self, body: LegacyLoginProbeBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbeData] | PendingApprovalResult[LegacyLoginProbeData]: ... + + def probe(self, body: LegacyLoginProbeBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbeData] | PendingApprovalResult[LegacyLoginProbeData]: + """Ask the declared legacy login endpoint whether it knows an address — your own — to test it before approving. Sends no password. + + Requires scope `signin:write`. Danger: write. + + ``POST /legacy-login/probe`` · action ``legacy_login.probe`` + Scope ``signin:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["legacy_login.probe"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin]: ... + + @overload + def revoke(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin]: ... + + def revoke(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin]: + """Withdraw the legacy login approval. People not yet migrated can no longer sign in; the declaration stays on file. + + Requires scope `signin:write`. Danger: critical. + + ``POST /legacy-login/revoke`` · action ``legacy_login.revoke`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["legacy_login.revoke"], (), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class LogStreamsMethods: + """``log_streams.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: LogStreamsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream]: ... + + @overload + def create(self, body: LogStreamsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: ... + + def create(self, body: LogStreamsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: + """Stream the audit trail to a SIEM (Splunk, Elastic, Graylog, CEF, JSON). A generated HMAC key is returned once. + + Requires scope `log_streams:write`. Danger: critical. + + ``POST /log-streams`` · action ``log_streams.create`` + Scope ``log_streams:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete an audit log stream. Nothing more is delivered to its endpoint. + + Requires scope `log_streams:write`. Danger: destructive. + + ``DELETE /log-streams/{id}`` · action ``log_streams.delete`` + Scope ``log_streams:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: + """Get one audit log stream: destination, endpoint, auth scheme, owner and health. Never its secret. + + Requires scope `log_streams:read`. Danger: read. + + ``GET /log-streams/{id}`` · action ``log_streams.get`` + Scope ``log_streams:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: LogStreamsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamsListData]: ... + + @overload + def list(self, query: LogStreamsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamsListData] | PendingApprovalResult[LogStreamsListData]: ... + + def list(self, query: LogStreamsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamsListData] | PendingApprovalResult[LogStreamsListData]: + """List the SIEM destinations this environment's audit trail is streamed to, and whether each is enabled. Never a secret. + + Requires scope `log_streams:read`. Danger: read. + + ``GET /log-streams`` · action ``log_streams.list`` + Scope ``log_streams:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: LogStreamsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[LogStream]: + """Every item of ``log_streams.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["log_streams.list"], (), query, headers=headers) + + @overload + def test(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamTest]: ... + + @overload + def test(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamTest] | PendingApprovalResult[LogStreamTest]: ... + + def test(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamTest] | PendingApprovalResult[LogStreamTest]: + """Send one test entry to a log stream now and report whether the SIEM accepted it. + + Requires scope `log_streams:write`. Danger: write. + + ``POST /log-streams/{id}/test`` · action ``log_streams.test`` + Scope ``log_streams:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.test"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, id: str, body: LogStreamsUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream]: ... + + @overload + def update(self, id: str, body: LogStreamsUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: ... + + def update(self, id: str, body: LogStreamsUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: + """Disable (enabled: false) or resume (enabled: true) an audit log stream. Disabled, entries are kept and delivered on resume. + + Requires scope `log_streams:write`. Danger: critical. + + ``PATCH /log-streams/{id}`` · action ``log_streams.update`` + Scope ``log_streams:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class MembersRolesMethods: + """``members.roles.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def grant(self, organization_id: str, user_id: str, role_id: str, query: MembersRolesGrantQuery | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment]: ... + + @overload + def grant(self, organization_id: str, user_id: str, role_id: str, query: MembersRolesGrantQuery | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment]: ... + + def grant(self, organization_id: str, user_id: str, role_id: str, query: MembersRolesGrantQuery | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment]: + """Grant a member a role in the organization + + Requires scope `roles:write`. Idempotent. + + **This API grants with the environment's authority.** It is your backend, so it may + grant any role the organization can hold — including a **staff role** + (`tenant_assignable: false`), such as your support lead's "Support" role at one + customer. An organization's own administrators still cannot: their console, their + invitations and their directory mappings refuse staff roles, and nothing here + changes that. + + Refusals: `422 role_not_assignable` (another organization's role, or one from an app + this organization cannot use), `409 role_conflict` (segregation of duties). + + ``PUT /organizations/{organization_id}/members/{user_id}/roles/{role_id}`` · action ``members.roles.grant`` + Scope ``roles:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.roles.grant"], (organization_id, user_id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, organization_id: str, user_id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[MembersRolesListData]: ... + + @overload + def list(self, organization_id: str, user_id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[MembersRolesListData] | PendingApprovalResult[MembersRolesListData]: ... + + def list(self, organization_id: str, user_id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[MembersRolesListData] | PendingApprovalResult[MembersRolesListData]: + """List the roles a member holds in the organization + + Requires scope `roles:read`. Roles granted AT this organization. Roles a person holds + everywhere in the environment (staff) are listed under + `/users/{id}/environment-roles`. + + ``GET /organizations/{organization_id}/members/{user_id}/roles`` · action ``members.roles.list`` + Scope ``roles:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.roles.list"], (organization_id, user_id), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, organization_id: str, user_id: str, role_id: str, query: MembersRolesRevokeQuery | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, organization_id: str, user_id: str, role_id: str, query: MembersRolesRevokeQuery | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, organization_id: str, user_id: str, role_id: str, query: MembersRolesRevokeQuery | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Take a role back from a member + + Requires scope `roles:write`. Idempotent — a role the member does not hold is a 204 too. + + ``DELETE /organizations/{organization_id}/members/{user_id}/roles/{role_id}`` · action ``members.roles.revoke`` + Scope ``roles:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.roles.revoke"], (organization_id, user_id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class MembersMethods: + """``members.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.roles = MembersRolesMethods(core) + + @overload + def add(self, organization_id: str, body: MembersAddBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def add(self, organization_id: str, body: MembersAddBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... + + def add(self, organization_id: str, body: MembersAddBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: + """Add an existing user as a member + + Requires scope `members:write`. Straight in — no invitation, no mail. Name the user by + `user_id` or by `email`; they must exist in this environment (`422 user_not_found`). + A suspended or archived organization takes no new members (`409 organization_inactive`). + + `roles` grants access roles at the same time — ids, or your app's manifest keys with + `client_id` — with the environment's authority (staff roles included). One the + organization cannot hold is `422 role_not_assignable`; a segregation-of-duties + conflict is `409 role_conflict`, and nothing is added. + + `role` is `admin` or `member` (default `member`). `owner` is never assignable: + ownership moves with `transfer-ownership`. + + **Idempotent for the same role**: repeating the request answers `200` with the + existing membership. A member on a *different* role is `409 already_member` — change + the role with `PATCH`. + + ``POST /organizations/{organization_id}/members`` · action ``members.add`` + Scope ``members:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.add"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, organization_id: str, query: MembersListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[MembersListData]: ... + + @overload + def list(self, organization_id: str, query: MembersListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[MembersListData] | PendingApprovalResult[MembersListData]: ... + + def list(self, organization_id: str, query: MembersListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[MembersListData] | PendingApprovalResult[MembersListData]: + """List an organization's members + + Requires scope `members:read`. Every membership, whatever its status, oldest first. + + ``GET /organizations/{organization_id}/members`` · action ``members.list`` + Scope ``members:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.list"], (organization_id,), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, organization_id: str, query: MembersListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Member]: + """Every item of ``members.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["members.list"], (organization_id,), query, headers=headers) + + @overload + def remove(self, organization_id: str, user_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, organization_id: str, user_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, organization_id: str, user_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a member + + Requires scope `members:write`. The member's roles in the organization go with the + membership. Removing the only owner is refused with `409 last_owner`. + + ``DELETE /organizations/{organization_id}/members/{user_id}`` · action ``members.remove`` + Scope ``members:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.remove"], (organization_id, user_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, organization_id: str, user_id: str, body: MembersUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def update(self, organization_id: str, user_id: str, body: MembersUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... + + def update(self, organization_id: str, user_id: str, body: MembersUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: + """Change a member's role + + Requires scope `members:write`. `role` is `admin` or `member`. Demoting the only + owner is refused with `409 last_owner` — transfer ownership first. + + ``PATCH /organizations/{organization_id}/members/{user_id}`` · action ``members.update`` + Scope ``members:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["members.update"], (organization_id, user_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class OrganizationsDomainsMethods: + """``organizations.domains.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def add(self, organization_id: str, body: OrganizationsDomainsAddBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain]: ... + + @overload + def add(self, organization_id: str, body: OrganizationsDomainsAddBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain]: ... + + def add(self, organization_id: str, body: OrganizationsDomainsAddBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain]: + """Claim an email domain (acme.com) for an organization. Returns the DNS TXT record to publish before verifying it. + + Requires scope `organizations:write`. Danger: write. + + ``POST /organizations/{organization_id}/domains`` · action ``organizations.domains.add`` + Scope ``organizations:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.domains.add"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def capture(self, organization_id: str, domain_id: str, body: OrganizationsDomainsCaptureBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain]: ... + + @overload + def capture(self, organization_id: str, domain_id: str, body: OrganizationsDomainsCaptureBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain]: ... + + def capture(self, organization_id: str, domain_id: str, body: OrganizationsDomainsCaptureBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain]: + """Turn capture on or off for a verified domain: with it on, everyone with an address there signs in through the organization's SSO. + + Requires scope `organizations:write`. Danger: critical. + + ``PUT /organizations/{organization_id}/domains/{domain_id}/capture`` · action ``organizations.domains.capture`` + Scope ``organizations:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.domains.capture"], (organization_id, domain_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, organization_id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsDomainsListData]: ... + + @overload + def list(self, organization_id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsDomainsListData] | PendingApprovalResult[OrganizationsDomainsListData]: ... + + def list(self, organization_id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsDomainsListData] | PendingApprovalResult[OrganizationsDomainsListData]: + """List the email domains an organization claims + + Requires scope `organizations:read`. Verified or not, each with the DNS TXT record + that proves it (`record_name`, `record_value`). A handful at most; not paged. + + ``GET /organizations/{organization_id}/domains`` · action ``organizations.domains.list`` + Scope ``organizations:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.domains.list"], (organization_id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def remove(self, organization_id: str, domain_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, organization_id: str, domain_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, organization_id: str, domain_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a claimed email domain from an organization. Capture on it ends. + + Requires scope `organizations:write`. Danger: destructive. + + ``DELETE /organizations/{organization_id}/domains/{domain_id}`` · action ``organizations.domains.remove`` + Scope ``organizations:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.domains.remove"], (organization_id, domain_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def verify(self, organization_id: str, domain_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain]: ... + + @overload + def verify(self, organization_id: str, domain_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain]: ... + + def verify(self, organization_id: str, domain_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain]: + """Check a claimed domain's DNS TXT record and mark the domain verified if it is published. + + Requires scope `organizations:write`. Danger: write. + + ``POST /organizations/{organization_id}/domains/{domain_id}/verify`` · action ``organizations.domains.verify`` + Scope ``organizations:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.domains.verify"], (organization_id, domain_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class OrganizationsPortalLinksMethods: + """``organizations.portal_links.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, organization_id: str, body: OrganizationsPortalLinksCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PortalLink]: ... + + @overload + def create(self, organization_id: str, body: OrganizationsPortalLinksCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PortalLink] | PendingApprovalResult[PortalLink]: ... + + def create(self, organization_id: str, body: OrganizationsPortalLinksCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PortalLink] | PendingApprovalResult[PortalLink]: + """Create a one-time Admin Portal link an organization's IT administrator uses to set up SSO, directory sync, domain verification, log streams or SAML certificate renewal, or to read its audit logs, without an account. The URL is shown once. + + Requires scope `portal_links:write`. Danger: critical. + + ``POST /organizations/{organization_id}/portal-links`` · action ``organizations.portal_links.create`` + Scope ``portal_links:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.portal_links.create"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class OrganizationsMethods: + """``organizations.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.domains = OrganizationsDomainsMethods(core) + self.portal_links = OrganizationsPortalLinksMethods(core) + + @overload + def create(self, body: OrganizationsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def create(self, body: OrganizationsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def create(self, body: OrganizationsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Create an organization, optionally with its owner + + Requires scope `organizations:write`. + + ``POST /organizations`` · action ``organizations.create`` + Scope ``organizations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Archive an organization + + Requires scope `organizations:write`. **Archives** rather than erases: the status + becomes `deleted`, every member loses access from their next request, the rows stay + for the audit trail, and `organization.deleted` is announced. Idempotent — archiving + an archived organization answers with it unchanged. + + Refused with `409 owns_products` for an organization that owns identity-provider + projects on this platform (a platform customer, not a tenant of your app). + + ``DELETE /organizations/{id}`` · action ``organizations.delete`` + Scope ``organizations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Get an organization + + Requires scope `organizations:read`. + + ``GET /organizations/{id}`` · action ``organizations.get`` + Scope ``organizations:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: OrganizationsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsListData]: ... + + @overload + def list(self, query: OrganizationsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsListData] | PendingApprovalResult[OrganizationsListData]: ... + + def list(self, query: OrganizationsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsListData] | PendingApprovalResult[OrganizationsListData]: + """List organizations + + Requires scope `organizations:read`. A page at a time, archived ones included. Narrow + with `q` (a fragment of the name or slug) or `status`. + + ``GET /organizations`` · action ``organizations.list`` + Scope ``organizations:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: OrganizationsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Organization]: + """Every item of ``organizations.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["organizations.list"], (), query, headers=headers) + + @overload + def reactivate(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def reactivate(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def reactivate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Lift an organization's suspension so its members can sign in again. + + Requires scope `organizations:write`. Danger: write. + + ``POST /organizations/{id}/reactivate`` · action ``organizations.reactivate`` + Scope ``organizations:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.reactivate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def suspend(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def suspend(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def suspend(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Suspend an organization: its members cannot sign in to it until it is reactivated. + + Requires scope `organizations:write`. Danger: write. + + ``POST /organizations/{id}/suspend`` · action ``organizations.suspend`` + Scope ``organizations:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.suspend"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def transfer_ownership(self, id: str, body: OrganizationsTransferOwnershipBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def transfer_ownership(self, id: str, body: OrganizationsTransferOwnershipBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... + + def transfer_ownership(self, id: str, body: OrganizationsTransferOwnershipBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: + """Make a member the organization's owner + + Requires scope `organizations:write`. The new owner must be an **active** member. + With one current owner this is a hand-over: the previous owner stays on as `admin`. + An organization with no owner (created without `owner_user_id`) gets its first; one + with several (from before ownership was transfer-only) ends with exactly one, the + others stepping down to `admin`. + + Refusals: `422 not_a_member`, `409 not_active`, `409 already_owner`. + + ``POST /organizations/{id}/transfer-ownership`` · action ``organizations.transfer_ownership`` + Scope ``organizations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.transfer_ownership"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, id: str, body: OrganizationsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def update(self, id: str, body: OrganizationsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def update(self, id: str, body: OrganizationsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Rename an organization, change its slug, or replace its metadata + + Requires scope `organizations:write`. A field left out is left alone; a request + that changes nothing answers with the organization as it is and records nothing. + A change is announced as the `organization.updated` webhook and recorded on the + organization's trail as `organization.renamed`, with the values before and after. + + ``PATCH /organizations/{id}`` · action ``organizations.update`` + Scope ``organizations:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class PermissionsMethods: + """``permissions.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: PermissionsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Permission]: ... + + @overload + def create(self, body: PermissionsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Permission] | PendingApprovalResult[Permission]: ... + + def create(self, body: PermissionsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Permission] | PendingApprovalResult[Permission]: + """Author a manual `feature:action` permission, shared with the environment (organization_id null) or one organization's own. + + Requires scope `role_definitions:write`. Danger: write. + + ``POST /permissions`` · action ``permissions.create`` + Scope ``role_definitions:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["permissions.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete a manual permission: every role carrying it loses it first. + + Requires scope `role_definitions:write`. Danger: destructive. + + ``DELETE /permissions/{id}`` · action ``permissions.delete`` + Scope ``role_definitions:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["permissions.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: PermissionsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PermissionsListData]: ... + + @overload + def list(self, query: PermissionsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PermissionsListData] | PendingApprovalResult[PermissionsListData]: ... + + def list(self, query: PermissionsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PermissionsListData] | PendingApprovalResult[PermissionsListData]: + """List the permission catalogue roles are composed from, a page at a time. Narrow by `client_id` (one app's), `organization_id` (what it can see) or a `q` fragment. + + Requires scope `roles:read`. Danger: read. + + ``GET /permissions`` · action ``permissions.list`` + Scope ``roles:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["permissions.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: PermissionsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Permission]: + """Every item of ``permissions.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["permissions.list"], (), query, headers=headers) + + @overload + def update(self, id: str, body: PermissionsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Permission]: ... + + @overload + def update(self, id: str, body: PermissionsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Permission] | PendingApprovalResult[Permission]: ... + + def update(self, id: str, body: PermissionsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Permission] | PendingApprovalResult[Permission]: + """Change a manual permission's description, and on the shared tier whether organizations may use it. The key itself never changes. + + Requires scope `role_definitions:write`. Danger: write. + + ``PATCH /permissions/{id}`` · action ``permissions.update`` + Scope ``role_definitions:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["permissions.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ProvisioningTargetsStatusMethods: + """``provisioning.targets.status.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, id: str, body: ProvisioningTargetsStatusSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget]: ... + + @overload + def set(self, id: str, body: ProvisioningTargetsStatusSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget]: ... + + def set(self, id: str, body: ProvisioningTargetsStatusSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget]: + """Pause or resume pushing people to a downstream SCIM target. + + Requires scope `provisioning:write`. Danger: write. + + ``POST /provisioning-targets/{id}/status`` · action ``provisioning.targets.status.set`` + Scope ``provisioning:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["provisioning.targets.status.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ProvisioningTargetsMethods: + """``provisioning.targets.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.status = ProvisioningTargetsStatusMethods(core) + + @overload + def create(self, body: ProvisioningTargetsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget]: ... + + @overload + def create(self, body: ProvisioningTargetsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget]: ... + + def create(self, body: ProvisioningTargetsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget]: + """Register a downstream app this platform pushes an organization's people to over SCIM, or every organization's with environment_wide. + + Requires scope `provisioning:write`. Danger: critical. + + ``POST /provisioning-targets`` · action ``provisioning.targets.create`` + Scope ``provisioning:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["provisioning.targets.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, body: ProvisioningTargetsDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, body: ProvisioningTargetsDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, body: ProvisioningTargetsDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete a downstream SCIM target. Nothing more is pushed to it. + + Requires scope `provisioning:write`. Danger: destructive. + + ``DELETE /provisioning-targets/{id}`` · action ``provisioning.targets.delete`` + Scope ``provisioning:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["provisioning.targets.delete"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: ProvisioningTargetsGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget]: ... + + @overload + def get(self, id: str, query: ProvisioningTargetsGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget]: ... + + def get(self, id: str, query: ProvisioningTargetsGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget]: + """Read one downstream SCIM target: its URL, auth scheme, status and last error. Never its credential. + + Requires scope `provisioning:read`. Danger: read. + + ``GET /provisioning-targets/{id}`` · action ``provisioning.targets.get`` + Scope ``provisioning:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["provisioning.targets.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: ProvisioningTargetsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTargetsListData]: ... + + @overload + def list(self, query: ProvisioningTargetsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTargetsListData] | PendingApprovalResult[ProvisioningTargetsListData]: ... + + def list(self, query: ProvisioningTargetsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisioningTargetsListData] | PendingApprovalResult[ProvisioningTargetsListData]: + """List the downstream SCIM targets people are provisioned to, optionally for one organization, with their failures. Never their credentials. + + Requires scope `provisioning:read`. Danger: read. + + ``GET /provisioning-targets`` · action ``provisioning.targets.list`` + Scope ``provisioning:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["provisioning.targets.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: ProvisioningTargetsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[ProvisioningTarget]: + """Every item of ``provisioning.targets.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["provisioning.targets.list"], (), query, headers=headers) + + +class ProvisioningMethods: + """``provisioning.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.targets = ProvisioningTargetsMethods(core) + + +class RolesPermissionsMethods: + """``roles.permissions.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def grant(self, id: str, permission_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role]: ... + + @overload + def grant(self, id: str, permission_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: ... + + def grant(self, id: str, permission_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: + """Add a permission to a role: everyone holding the role gains it. The permission must be unscoped or declared by the role's own app. + + Requires scope `role_definitions:write`. Danger: write. + + ``PUT /roles/{id}/permissions/{permission_id}`` · action ``roles.permissions.grant`` + Scope ``role_definitions:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.permissions.grant"], (id, permission_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, id: str, permission_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role]: ... + + @overload + def revoke(self, id: str, permission_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: ... + + def revoke(self, id: str, permission_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: + """Remove a permission from a role: everyone holding the role loses it. + + Requires scope `role_definitions:write`. Danger: destructive. + + ``DELETE /roles/{id}/permissions/{permission_id}`` · action ``roles.permissions.revoke`` + Scope ``role_definitions:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.permissions.revoke"], (id, permission_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class RolesMethods: + """``roles.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.permissions = RolesPermissionsMethods(core) + + @overload + def create(self, body: RolesCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role]: ... + + @overload + def create(self, body: RolesCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: ... + + def create(self, body: RolesCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: + """Define a role for one organization, or (organization_id null) for the whole environment, optionally scoped to one app, with its opening permissions. + + Requires scope `role_definitions:write`. Danger: write. + + ``POST /roles`` · action ``roles.create`` + Scope ``role_definitions:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete a role: everyone who holds it loses it. An app-declared role is removed from the app's manifest instead. + + Requires scope `role_definitions:write`. Danger: destructive. + + ``DELETE /roles/{id}`` · action ``roles.delete`` + Scope ``role_definitions:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: RolesGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role]: ... + + @overload + def get(self, id: str, query: RolesGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: ... + + def get(self, id: str, query: RolesGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: + """Get one role and the permissions it carries. Name it by id, or by manifest `key` with `client_id`. + + Requires scope `roles:read`. Danger: read. + + ``GET /roles/{id}`` · action ``roles.get`` + Scope ``roles:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: RolesListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RolesListData]: ... + + @overload + def list(self, query: RolesListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RolesListData] | PendingApprovalResult[RolesListData]: ... + + def list(self, query: RolesListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RolesListData] | PendingApprovalResult[RolesListData]: + """List roles + + Requires scope `roles:read`. Every role that can still be granted, ordered by name, + with the permissions each carries. Not paginated. `?client_id=` narrows to one + app's roles; `?organization_id=` to what can be granted in that organization. + + ``GET /roles`` · action ``roles.list`` + Scope ``roles:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def update(self, id: str, body: RolesUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role]: ... + + @overload + def update(self, id: str, body: RolesUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: ... + + def update(self, id: str, body: RolesUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Role] | PendingApprovalResult[Role]: + """Rename a role or change its description. An app-declared role is changed in the app's manifest instead. + + Requires scope `role_definitions:write`. Danger: write. + + ``PATCH /roles/{id}`` · action ``roles.update`` + Scope ``role_definitions:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["roles.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SamlAppsMethods: + """``saml_apps.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: SamlAppsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp]: ... + + @overload + def create(self, body: SamlAppsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp]: ... + + def create(self, body: SamlAppsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp]: + """Register a SAML app that signs people in with their account here. Decides where assertions — and their attributes — are sent. + + Requires scope `saml_apps:write`. Danger: critical. + + ``POST /saml-apps`` · action ``saml_apps.create`` + Scope ``saml_apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["saml_apps.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a SAML app. People can no longer sign in to it with their account here. + + Requires scope `saml_apps:write`. Danger: critical. + + ``DELETE /saml-apps/{id}`` · action ``saml_apps.delete`` + Scope ``saml_apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["saml_apps.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp]: + """Read one SAML app: its entity id, ACS URL, NameID and attribute mappings. Never its certificate. + + Requires scope `saml_apps:read`. Danger: read. + + ``GET /saml-apps/{id}`` · action ``saml_apps.get`` + Scope ``saml_apps:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["saml_apps.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: SamlAppsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlAppsListData]: ... + + @overload + def list(self, query: SamlAppsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlAppsListData] | PendingApprovalResult[SamlAppsListData]: ... + + def list(self, query: SamlAppsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlAppsListData] | PendingApprovalResult[SamlAppsListData]: + """List the SAML apps that trust this environment as their identity provider. + + Requires scope `saml_apps:read`. Danger: read. + + ``GET /saml-apps`` · action ``saml_apps.list`` + Scope ``saml_apps:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["saml_apps.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: SamlAppsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[SamlApp]: + """Every item of ``saml_apps.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["saml_apps.list"], (), query, headers=headers) + + @overload + def update(self, id: str, body: SamlAppsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp]: ... + + @overload + def update(self, id: str, body: SamlAppsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp]: ... + + def update(self, id: str, body: SamlAppsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp]: + """Change a SAML app's entity id, ACS URL, NameID, attribute mappings, signing certificate or owning organization. + + Requires scope `saml_apps:write`. Danger: critical. + + ``PATCH /saml-apps/{id}`` · action ``saml_apps.update`` + Scope ``saml_apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["saml_apps.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SigninPolicyMethods: + """``signin.policy.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def get(self, query: SigninPolicyGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy]: ... + + @overload + def get(self, query: SigninPolicyGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy]: ... + + def get(self, query: SigninPolicyGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy]: + """Read the authentication policy (password, MFA, SSO, lockout): the environment baseline, or one organization's effective rules and override. + + Requires scope `signin:read`. Danger: read. + + ``GET /sign-in/policy`` · action ``signin.policy.get`` + Scope ``signin:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.policy.get"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def inherit(self, organization_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def inherit(self, organization_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def inherit(self, organization_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Drop one organization's authentication policy override, so it inherits the environment baseline again. + + Requires scope `signin:write`. Danger: critical. + + ``DELETE /sign-in/policy/organizations/{organization_id}`` · action ``signin.policy.inherit`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.policy.inherit"], (organization_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, body: SigninPolicyUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy]: ... + + @overload + def update(self, body: SigninPolicyUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy]: ... + + def update(self, body: SigninPolicyUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy]: + """Change the authentication policy of the environment baseline, or tighten one organization's override. Requiring SSO signs out password sessions. + + Requires scope `signin:write`. Danger: critical. + + ``PATCH /sign-in/policy`` · action ``signin.policy.update`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.policy.update"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SigninSelfServiceSignupMethods: + """``signin.self_service_signup.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, body: SigninSelfServiceSignupSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSelfServiceSignupSetData]: ... + + @overload + def set(self, body: SigninSelfServiceSignupSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSelfServiceSignupSetData] | PendingApprovalResult[SigninSelfServiceSignupSetData]: ... + + def set(self, body: SigninSelfServiceSignupSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSelfServiceSignupSetData] | PendingApprovalResult[SigninSelfServiceSignupSetData]: + """Switch self-service sign-up on or off: whether people can create an account and their own organization in this environment. + + Requires scope `signin:write`. Danger: critical. + + ``PUT /sign-in/self-service-signup`` · action ``signin.self_service_signup.set`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.self_service_signup.set"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SigninSocialMethods: + """``signin.social.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def delete(self, id: str, body: SigninSocialDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, body: SigninSocialDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, body: SigninSocialDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Stop offering a social login provider. People who used it keep their accounts. + + Requires scope `signin:write`. Danger: critical. + + ``DELETE /sign-in/social-providers/{id}`` · action ``signin.social.delete`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.social.delete"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: SigninSocialListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSocialListData]: ... + + @overload + def list(self, query: SigninSocialListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSocialListData] | PendingApprovalResult[SigninSocialListData]: ... + + def list(self, query: SigninSocialListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSocialListData] | PendingApprovalResult[SigninSocialListData]: + """List the social login providers (Google, GitHub, Apple…) enabled in this environment, optionally for one organization. + + Requires scope `signin:read`. Danger: read. + + ``GET /sign-in/social-providers`` · action ``signin.social.list`` + Scope ``signin:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.social.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: SigninSocialListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[SocialProvider]: + """Every item of ``signin.social.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["signin.social.list"], (), query, headers=headers) + + @overload + def set(self, body: SigninSocialSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SocialProvider]: ... + + @overload + def set(self, body: SigninSocialSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SocialProvider] | PendingApprovalResult[SocialProvider]: ... + + def set(self, body: SigninSocialSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SocialProvider] | PendingApprovalResult[SocialProvider]: + """Enable a social login provider (Google, GitHub, Apple…) for one organization with its client credentials. The secret is never returned. + + Requires scope `signin:write`. Danger: critical. + + ``POST /sign-in/social-providers`` · action ``signin.social.set`` + Scope ``signin:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["signin.social.set"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SigninMethods: + """``signin.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.policy = SigninPolicyMethods(core) + self.self_service_signup = SigninSelfServiceSignupMethods(core) + self.social = SigninSocialMethods(core) + + +class SodPoliciesStatusMethods: + """``sod_policies.status.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, id: str, body: SodPoliciesStatusSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy]: ... + + @overload + def set(self, id: str, body: SodPoliciesStatusSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy]: ... + + def set(self, id: str, body: SodPoliciesStatusSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy]: + """Enforce a role-conflict rule, or stop enforcing it — switched off, grants that break it are allowed again. + + Requires scope `governance:write`. Danger: critical. + + ``POST /sod-policies/{id}/status`` · action ``sod_policies.status.set`` + Scope ``governance:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sod_policies.status.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SodPoliciesMethods: + """``sod_policies.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.status = SodPoliciesStatusMethods(core) + + @overload + def create(self, body: SodPoliciesCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy]: ... + + @overload + def create(self, body: SodPoliciesCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy]: ... + + def create(self, body: SodPoliciesCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy]: + """Define a role-conflict rule: two or more roles no one person may hold together. Enforced on every grant from now on. + + Requires scope `governance:write`. Danger: write. + + ``POST /sod-policies`` · action ``sod_policies.create`` + Scope ``governance:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sod_policies.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, body: SodPoliciesDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, body: SodPoliciesDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, body: SodPoliciesDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a role-conflict rule. Grants it refused are allowed from then on. + + Requires scope `governance:write`. Danger: critical. + + ``DELETE /sod-policies/{id}`` · action ``sod_policies.delete`` + Scope ``governance:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sod_policies.delete"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: SodPoliciesGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy]: ... + + @overload + def get(self, id: str, query: SodPoliciesGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy]: ... + + def get(self, id: str, query: SodPoliciesGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy]: + """Read one role-conflict rule: the roles no one person may hold together, and whether it is enforced. + + Requires scope `governance:read`. Danger: read. + + ``GET /sod-policies/{id}`` · action ``sod_policies.get`` + Scope ``governance:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sod_policies.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: SodPoliciesListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPoliciesListData]: ... + + @overload + def list(self, query: SodPoliciesListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPoliciesListData] | PendingApprovalResult[SodPoliciesListData]: ... + + def list(self, query: SodPoliciesListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SodPoliciesListData] | PendingApprovalResult[SodPoliciesListData]: + """List role-conflict (segregation of duties) rules: sets of roles no one person may hold together. Optionally those binding one organization. + + Requires scope `governance:read`. Danger: read. + + ``GET /sod-policies`` · action ``sod_policies.list`` + Scope ``governance:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sod_policies.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: SodPoliciesListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[SodPolicy]: + """Every item of ``sod_policies.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["sod_policies.list"], (), query, headers=headers) + + +class SsoConnectionsCertificatesMethods: + """``sso.connections.certificates.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def activate(self, id: str, body: SsoConnectionsCertificatesActivateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates]: ... + + @overload + def activate(self, id: str, body: SsoConnectionsCertificatesActivateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates]: ... + + def activate(self, id: str, body: SsoConnectionsCertificatesActivateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates]: + """Make a staged SAML signing certificate the connection's primary. The certificate it replaces stops being trusted. + + Requires scope `sso:write`. Danger: critical. + + ``POST /sso/connections/{id}/certificates/activate`` · action ``sso.connections.certificates.activate`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.certificates.activate"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, id: str, query: SsoConnectionsCertificatesListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates]: ... + + @overload + def list(self, id: str, query: SsoConnectionsCertificatesListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates]: ... + + def list(self, id: str, query: SsoConnectionsCertificatesListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates]: + """List a SAML connection's signing certificates — primary and staged — with each one's fingerprint and expiry. + + Requires scope `sso:read`. Danger: read. + + ``GET /sso/connections/{id}/certificates`` · action ``sso.connections.certificates.list`` + Scope ``sso:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.certificates.list"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def stage(self, id: str, body: SsoConnectionsCertificatesStageBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates]: ... + + @overload + def stage(self, id: str, body: SsoConnectionsCertificatesStageBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates]: ... + + def stage(self, id: str, body: SsoConnectionsCertificatesStageBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates]: + """Stage a SAML connection's new IdP signing certificate (PEM or IdP metadata) beside the current one, so a renewal has no outage. Activate it once the IdP signs with it. + + Requires scope `sso:write`. Danger: critical. + + ``POST /sso/connections/{id}/certificates`` · action ``sso.connections.certificates.stage`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.certificates.stage"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SsoConnectionsMethods: + """``sso.connections.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.certificates = SsoConnectionsCertificatesMethods(core) + + @overload + def activate(self, id: str, body: SsoConnectionsActivateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection]: ... + + @overload + def activate(self, id: str, body: SsoConnectionsActivateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: ... + + def activate(self, id: str, body: SsoConnectionsActivateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: + """Activate an SSO connection: people whose email domain routes to it start signing in through it. + + Requires scope `sso:write`. Danger: critical. + + ``POST /sso/connections/{id}/activate`` · action ``sso.connections.activate`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.activate"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def create(self, body: SsoConnectionsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection]: ... + + @overload + def create(self, body: SsoConnectionsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: ... + + def create(self, body: SsoConnectionsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: + """Connect an organization's SAML or OIDC identity provider as a draft. Activate it to start signing people in through it. + + Requires scope `sso:write`. Danger: write. + + ``POST /sso/connections`` · action ``sso.connections.create`` + Scope ``sso:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, body: SsoConnectionsDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, body: SsoConnectionsDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, body: SsoConnectionsDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete an SSO connection and its settings. Where SSO is required, its people cannot sign in until another is activated. + + Requires scope `sso:write`. Danger: critical. + + ``DELETE /sso/connections/{id}`` · action ``sso.connections.delete`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.delete"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def disable(self, id: str, body: SsoConnectionsDisableBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection]: ... + + @overload + def disable(self, id: str, body: SsoConnectionsDisableBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: ... + + def disable(self, id: str, body: SsoConnectionsDisableBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: + """Disable an SSO connection without deleting it. Where SSO is required, its people cannot sign in until it is re-activated. + + Requires scope `sso:write`. Danger: critical. + + ``POST /sso/connections/{id}/disable`` · action ``sso.connections.disable`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.disable"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: SsoConnectionsGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection]: ... + + @overload + def get(self, id: str, query: SsoConnectionsGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: ... + + def get(self, id: str, query: SsoConnectionsGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: + """Read one SSO connection: its type, status, entity ids, URLs, issuer and client id. Never a certificate or secret. + + Requires scope `sso:read`. Danger: read. + + ``GET /sso/connections/{id}`` · action ``sso.connections.get`` + Scope ``sso:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: SsoConnectionsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnectionsListData]: ... + + @overload + def list(self, query: SsoConnectionsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnectionsListData] | PendingApprovalResult[SsoConnectionsListData]: ... + + def list(self, query: SsoConnectionsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnectionsListData] | PendingApprovalResult[SsoConnectionsListData]: + """List the SAML and OIDC connections organizations sign in through, optionally for one organization. Never a certificate or secret. + + Requires scope `sso:read`. Danger: read. + + ``GET /sso/connections`` · action ``sso.connections.list`` + Scope ``sso:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: SsoConnectionsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[SsoConnection]: + """Every item of ``sso.connections.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["sso.connections.list"], (), query, headers=headers) + + @overload + def require_sso(self, id: str, body: SsoConnectionsRequireSsoBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy]: ... + + @overload + def require_sso(self, id: str, body: SsoConnectionsRequireSsoBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy]: ... + + def require_sso(self, id: str, body: SsoConnectionsRequireSsoBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy]: + """Require single sign-on for the organization an SSO connection belongs to. Ends every password session there. + + Requires scope `sso:write`. Danger: critical. + + ``POST /sso/connections/{id}/require-sso`` · action ``sso.connections.require_sso`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.require_sso"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, id: str, body: SsoConnectionsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection]: ... + + @overload + def update(self, id: str, body: SsoConnectionsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: ... + + def update(self, id: str, body: SsoConnectionsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection]: + """Change an SSO connection's name or identity-provider settings. Secrets left out keep the ones on file. + + Requires scope `sso:write`. Danger: critical. + + ``PATCH /sso/connections/{id}`` · action ``sso.connections.update`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.connections.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SsoDomainsMethods: + """``sso.domains.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def capture(self, id: str, body: SsoDomainsCaptureBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain]: ... + + @overload + def capture(self, id: str, body: SsoDomainsCaptureBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain]: ... + + def capture(self, id: str, body: SsoDomainsCaptureBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain]: + """Turn capture on or off for a verified domain: captured, everyone with an address at it must sign in through the organization's SSO. + + Requires scope `sso:write`. Danger: critical. + + ``POST /sso/domains/{id}/capture`` · action ``sso.domains.capture`` + Scope ``sso:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.domains.capture"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def create(self, body: SsoDomainsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain]: ... + + @overload + def create(self, body: SsoDomainsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain]: ... + + def create(self, body: SsoDomainsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain]: + """Claim an email domain for an organization's single sign-on, and get the DNS TXT record to publish to prove it. + + Requires scope `sso:write`. Danger: write. + + ``POST /sso/domains`` · action ``sso.domains.create`` + Scope ``sso:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.domains.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, body: SsoDomainsDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, body: SsoDomainsDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, body: SsoDomainsDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a claimed email domain. Its people stop being routed to the organization's SSO connection. + + Requires scope `sso:write`. Danger: destructive. + + ``DELETE /sso/domains/{id}`` · action ``sso.domains.delete`` + Scope ``sso:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.domains.delete"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: SsoDomainsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomainsListData]: ... + + @overload + def list(self, query: SsoDomainsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomainsListData] | PendingApprovalResult[SsoDomainsListData]: ... + + def list(self, query: SsoDomainsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomainsListData] | PendingApprovalResult[SsoDomainsListData]: + """List the email domains organizations claimed for single sign-on, whether each is verified and captured, and the DNS record that proves an unverified one. + + Requires scope `sso:read`. Danger: read. + + ``GET /sso/domains`` · action ``sso.domains.list`` + Scope ``sso:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.domains.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: SsoDomainsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[SsoDomain]: + """Every item of ``sso.domains.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["sso.domains.list"], (), query, headers=headers) + + @overload + def verify(self, id: str, body: SsoDomainsVerifyBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain]: ... + + @overload + def verify(self, id: str, body: SsoDomainsVerifyBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain]: ... + + def verify(self, id: str, body: SsoDomainsVerifyBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain]: + """Check a claimed domain's DNS TXT record now. Answers the domain, with verified true once the record is found. + + Requires scope `sso:write`. Danger: write. + + ``POST /sso/domains/{id}/verify`` · action ``sso.domains.verify`` + Scope ``sso:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.domains.verify"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SsoSamlMetadataMethods: + """``sso.saml_metadata.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def import_(self, body: SsoSamlMetadataImportBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlMetadata]: ... + + @overload + def import_(self, body: SsoSamlMetadataImportBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlMetadata] | PendingApprovalResult[SamlMetadata]: ... + + def import_(self, body: SsoSamlMetadataImportBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SamlMetadata] | PendingApprovalResult[SamlMetadata]: + """Parse an identity provider's SAML metadata (XML or a metadata URL) into the entity id, SSO URL and certificate a SAML connection needs. Stores nothing. + + Requires scope `sso:write`. Danger: write. + + ``POST /sso/saml-metadata`` · action ``sso.saml_metadata.import`` + Scope ``sso:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["sso.saml_metadata.import"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class SsoMethods: + """``sso.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.connections = SsoConnectionsMethods(core) + self.domains = SsoDomainsMethods(core) + self.saml_metadata = SsoSamlMetadataMethods(core) + + +class SupportSessionsMethods: + """``support_sessions.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def end(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def end(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def end(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """End a support session now: every token it issued is revoked. + + Requires scope `support:write`. Danger: destructive. + + ``DELETE /support-sessions/{id}`` · action ``support_sessions.end`` + Scope ``support:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["support_sessions.end"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def start(self, body: SupportSessionsStartBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SupportSession]: ... + + @overload + def start(self, body: SupportSessionsStartBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SupportSession] | PendingApprovalResult[SupportSession]: ... + + def start(self, body: SupportSessionsStartBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SupportSession] | PendingApprovalResult[SupportSession]: + """Start a support session + + Requires scope `support:write`. A member of your staff (`actor_user_id`) signs in to + one of your apps (`client_id`) AS one of a customer's users (`user_id` in + `organization_id`), for a stated `reason`, for at most 60 minutes. + + The actor must hold your app's own `support:impersonate` permission through an + **environment-wide** grant (see `PUT /users/{id}/environment-roles/{role_id}`); + otherwise `403 not_permitted`. The app must be a first-party app the environment + owns that uses the authorization-code grant (`422 client_not_eligible`). The target + must be an active member (`422 target_not_member`) of an active organization + (`409 organization_inactive`). + + Send `redirect_uri` (registered for the app) and a PKCE S256 `code_challenge` to get + the first authorization `code` in the response. Your app redeems it at `/oauth/token` + with the verifier; every token carries `act: {"sub": actor_user_id}` and there is + never a refresh token. The session is audited on the customer's trail and the + environment's, and announced as the `support_session.started` webhook. + + `scopes` in the response are exactly what every token of the session carries: the + ones asked for (or, with none, the app's registration), never `offline_access`, and + audienced the way the token endpoint audiences them — once any of them belongs to a + registered API, the token is for that API and a scope no API registered (such as + `apps.manifest`) is left out. Scopes of two registered APIs cannot be audienced to + one token and are refused before the session starts (`422 invalid_target`). + + ``POST /support-sessions`` · action ``support_sessions.start`` + Scope ``support:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["support_sessions.start"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class TokenVaultGrantsMethods: + """``token_vault.grants.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, id: str, body: TokenVaultGrantsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret]: ... + + @overload + def create(self, id: str, body: TokenVaultGrantsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: ... + + def create(self, id: str, body: TokenVaultGrantsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: + """Grant an app (by OAuth client id) the right to lease a stored credential. + + Requires scope `token_vault:write`. Danger: critical. + + ``POST /token-vault/secrets/{id}/grants`` · action ``token_vault.grants.create`` + Scope ``token_vault:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.grants.create"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, client_id: str, body: TokenVaultGrantsDeleteBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, client_id: str, body: TokenVaultGrantsDeleteBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, client_id: str, body: TokenVaultGrantsDeleteBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Withdraw an app's right to lease a stored credential. + + Requires scope `token_vault:write`. Danger: destructive. + + ``DELETE /token-vault/secrets/{id}/grants/{client_id}`` · action ``token_vault.grants.delete`` + Scope ``token_vault:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.grants.delete"], (id, client_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class TokenVaultSecretsMethods: + """``token_vault.secrets.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: TokenVaultSecretsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret]: ... + + @overload + def create(self, body: TokenVaultSecretsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: ... + + def create(self, body: TokenVaultSecretsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: + """Store a downstream credential (a third-party API key) in an organization's token vault, or the environment's own. The value is sealed and never returned. + + Requires scope `token_vault:write`. Danger: write. + + ``POST /token-vault/secrets`` · action ``token_vault.secrets.create`` + Scope ``token_vault:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.secrets.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, query: TokenVaultSecretsGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret]: ... + + @overload + def get(self, id: str, query: TokenVaultSecretsGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: ... + + def get(self, id: str, query: TokenVaultSecretsGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: + """Read one stored credential and the client ids granted to lease it. Never its value. + + Requires scope `token_vault:read`. Danger: read. + + ``GET /token-vault/secrets/{id}`` · action ``token_vault.secrets.get`` + Scope ``token_vault:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.secrets.get"], (id,), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: TokenVaultSecretsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecretsListData]: ... + + @overload + def list(self, query: TokenVaultSecretsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecretsListData] | PendingApprovalResult[TokenVaultSecretsListData]: ... + + def list(self, query: TokenVaultSecretsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecretsListData] | PendingApprovalResult[TokenVaultSecretsListData]: + """List the downstream credentials stored in an organization's token vault (or the environment's own): names, providers, status. Never a value. + + Requires scope `token_vault:read`. Danger: read. + + ``GET /token-vault/secrets`` · action ``token_vault.secrets.list`` + Scope ``token_vault:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.secrets.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: TokenVaultSecretsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[TokenVaultSecret]: + """Every item of ``token_vault.secrets.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["token_vault.secrets.list"], (), query, headers=headers) + + @overload + def revoke(self, id: str, body: TokenVaultSecretsRevokeBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret]: ... + + @overload + def revoke(self, id: str, body: TokenVaultSecretsRevokeBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: ... + + def revoke(self, id: str, body: TokenVaultSecretsRevokeBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: + """Revoke a stored credential permanently. No app can lease it again. + + Requires scope `token_vault:write`. Danger: destructive. + + ``POST /token-vault/secrets/{id}/revoke`` · action ``token_vault.secrets.revoke`` + Scope ``token_vault:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.secrets.revoke"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def rotate(self, id: str, body: TokenVaultSecretsRotateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret]: ... + + @overload + def rotate(self, id: str, body: TokenVaultSecretsRotateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: ... + + def rotate(self, id: str, body: TokenVaultSecretsRotateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret]: + """Replace a stored credential's value. Every later lease hands out the new one. The value is never returned. + + Requires scope `token_vault:write`. Danger: critical. + + ``POST /token-vault/secrets/{id}/rotate`` · action ``token_vault.secrets.rotate`` + Scope ``token_vault:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["token_vault.secrets.rotate"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class TokenVaultMethods: + """``token_vault.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.grants = TokenVaultGrantsMethods(core) + self.secrets = TokenVaultSecretsMethods(core) + + +class UsersEnvironmentRolesMethods: + """``users.environment_roles.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def get(self, id: str, role_id: str, query: UsersEnvironmentRolesGetQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment]: ... + + @overload + def get(self, id: str, role_id: str, query: UsersEnvironmentRolesGetQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment]: ... + + def get(self, id: str, role_id: str, query: UsersEnvironmentRolesGetQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment]: + """Whether a user holds a role everywhere + + Requires scope `roles:read`. `200` with the grant, or `404` when it is not held. + + ``GET /users/{id}/environment-roles/{role_id}`` · action ``users.environment_roles.get`` + Scope ``roles:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.get"], (id, role_id), query, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def grant(self, id: str, role_id: str, query: UsersEnvironmentRolesGrantQuery | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment]: ... + + @overload + def grant(self, id: str, role_id: str, query: UsersEnvironmentRolesGrantQuery | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment]: ... + + def grant(self, id: str, role_id: str, query: UsersEnvironmentRolesGrantQuery | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment]: + """Grant a role everywhere in the environment (staff) + + Requires scope `roles:write`. Idempotent. For your own people who act across every + customer. Any role that belongs to no organization may be granted this way: an + app-agnostic one reaches every app's tokens, one your app declared reaches only + your app's. An organization's own role is refused with `422 role_not_assignable`; a + grant that would form a segregation-of-duties conflict in any organization the + person belongs to with `409 role_conflict`. + + ``PUT /users/{id}/environment-roles/{role_id}`` · action ``users.environment_roles.grant`` + Scope ``roles:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.grant"], (id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersEnvironmentRolesListData]: ... + + @overload + def list(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersEnvironmentRolesListData] | PendingApprovalResult[UsersEnvironmentRolesListData]: ... + + def list(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersEnvironmentRolesListData] | PendingApprovalResult[UsersEnvironmentRolesListData]: + """List the roles a user holds everywhere (staff) + + Requires scope `roles:read`. + + ``GET /users/{id}/environment-roles`` · action ``users.environment_roles.list`` + Scope ``roles:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.list"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, id: str, role_id: str, query: UsersEnvironmentRolesRevokeQuery | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, role_id: str, query: UsersEnvironmentRolesRevokeQuery | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, role_id: str, query: UsersEnvironmentRolesRevokeQuery | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Take back an environment-wide grant + + Requires scope `roles:write`. Idempotent. + + ``DELETE /users/{id}/environment-roles/{role_id}`` · action ``users.environment_roles.revoke`` + Scope ``roles:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.revoke"], (id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class UsersMfaMethods: + """``users.mfa.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def reset(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def reset(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def reset(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Reset a user's two-factor authentication: their authenticator and recovery codes are removed and they must enrol again. + + Requires scope `users:write`. Danger: critical. + + ``DELETE /users/{id}/mfa`` · action ``users.mfa.reset`` + Scope ``users:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.mfa.reset"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class UsersPasswordMethods: + """``users.password.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def set(self, id: str, body: UsersPasswordSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def set(self, id: str, body: UsersPasswordSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def set(self, id: str, body: UsersPasswordSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Set a user's password, temporary by default, and mail it to them unless told not to. Signs them out according to `revoke`. + + Requires scope `users:write`. Danger: critical. + + ``POST /users/{id}/password`` · action ``users.password.set`` + Scope ``users:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.password.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class UsersPasswordResetMethods: + """``users.password_reset.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def send(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def send(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def send(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Mail a user a password-reset link at their own address. + + Requires scope `users:write`. Danger: write. + + ``POST /users/{id}/password-reset`` · action ``users.password_reset.send`` + Scope ``users:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.password_reset.send"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class UsersSessionsMethods: + """``users.sessions.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def list(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersSessionsListData]: ... + + @overload + def list(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersSessionsListData] | PendingApprovalResult[UsersSessionsListData]: ... + + def list(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersSessionsListData] | PendingApprovalResult[UsersSessionsListData]: + """List a user's live sign-in sessions + + Requires scope `users:read`. The fifty most recently active, newest first — enough to + recognise a device and end one (`DELETE /users/{id}/sessions/{session_id}`), not a log. + `impersonation` marks a session somebody else opened as this person. + + ``GET /users/{id}/sessions`` · action ``users.sessions.list`` + Scope ``users:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.sessions.list"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, id: str, session_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, session_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, session_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """End one of a user's sign-in sessions. + + Requires scope `users:write`. Danger: destructive. + + ``DELETE /users/{id}/sessions/{session_id}`` · action ``users.sessions.revoke`` + Scope ``users:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.sessions.revoke"], (id, session_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke_all(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke_all(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke_all(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Sign a user out everywhere: end every session and revoke every OAuth grant they hold. + + Requires scope `users:write`. Danger: destructive. + + ``DELETE /users/{id}/sessions`` · action ``users.sessions.revoke_all`` + Scope ``users:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.sessions.revoke_all"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class UsersVerificationMethods: + """``users.verification.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def send(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def send(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def send(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Mail a user the link that verifies their email address. Does nothing for an address already verified. + + Requires scope `users:write`. Danger: write. + + ``POST /users/{id}/verification`` · action ``users.verification.send`` + Scope ``users:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.verification.send"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class UsersMethods: + """``users.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.environment_roles = UsersEnvironmentRolesMethods(core) + self.mfa = UsersMfaMethods(core) + self.password = UsersPasswordMethods(core) + self.password_reset = UsersPasswordResetMethods(core) + self.sessions = UsersSessionsMethods(core) + self.verification = UsersVerificationMethods(core) + + @overload + def create(self, body: UsersCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def create(self, body: UsersCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def create(self, body: UsersCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Create a user + + Requires scope `users:write`. Password is optional — omit it to create a passwordless identity the user completes via an invite/magic-link. + + ``POST /users`` · action ``users.create`` + Scope ``users:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def deactivate(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def deactivate(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def deactivate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Deactivate a user + + Requires scope `users:write`. A **soft disable** — the identity can no longer + authenticate, but is never hard-deleted — and every OAuth grant they hold is + revoked, which reactivating them does not bring back. Returns the user in its new + `disabled` state. Idempotent. The console's "Deactivate" is this action. + + ``DELETE /users/{id}`` · action ``users.deactivate`` + Scope ``users:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.deactivate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def erase(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ErasureReceipt]: ... + + @overload + def erase(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ErasureReceipt] | PendingApprovalResult[ErasureReceipt]: ... + + def erase(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ErasureReceipt] | PendingApprovalResult[ErasureReceipt]: + """Erase a person (GDPR Art. 17): revoke their sessions and tokens, delete their credentials, memberships and personal data, and pseudonymise their account. Cannot be undone. + + Requires scope `users:erase`. Danger: critical. + + ``POST /users/{id}/erase`` · action ``users.erase`` + Scope ``users:erase`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.erase"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Get a user + + Requires scope `users:read`. + + ``GET /users/{id}`` · action ``users.get`` + Scope ``users:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: UsersListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersListData]: ... + + @overload + def list(self, query: UsersListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersListData] | PendingApprovalResult[UsersListData]: ... + + def list(self, query: UsersListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[UsersListData] | PendingApprovalResult[UsersListData]: + """List users + + Requires scope `users:read`. A page at a time, in id order. Narrow it the ways you + look for somebody: `email` (exactly that address, case-insensitive), `q` (a fragment + of the address or the name) and `status`. Filters combine. + + ``GET /users`` · action ``users.list`` + Scope ``users:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: UsersListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[User]: + """Every item of ``users.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["users.list"], (), query, headers=headers) + + @overload + def reactivate(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def reactivate(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def reactivate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Reactivate a deactivated user so they can sign in again. Grants revoked on deactivation stay revoked. + + Requires scope `users:write`. Danger: write. + + ``POST /users/{id}/reactivate`` · action ``users.reactivate`` + Scope ``users:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.reactivate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, id: str, body: UsersUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def update(self, id: str, body: UsersUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def update(self, id: str, body: UsersUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Change a user's name and/or email address. A changed address must be verified again. + + Requires scope `users:write`. Danger: critical. + + ``PATCH /users/{id}`` · action ``users.update`` + Scope ``users:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def verify(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User]: ... + + @overload + def verify(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: ... + + def verify(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[User] | PendingApprovalResult[User]: + """Mark a user's email address verified without the emailed link. It can then be used to recover the account. + + Requires scope `users:write`. Danger: critical. + + ``POST /users/{id}/verify`` · action ``users.verify`` + Scope ``users:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["users.verify"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class WebhooksSecretMethods: + """``webhooks.secret.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def rotate(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def rotate(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def rotate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Issue a new signing secret for a webhook endpoint, returned once. The old secret stops verifying immediately. + + Requires scope `webhooks:write`. Danger: critical. + + ``POST /webhooks/{id}/rotate`` · action ``webhooks.secret.rotate`` + Scope ``webhooks:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.secret.rotate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class WebhooksMethods: + """``webhooks.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.secret = WebhooksSecretMethods(core) + + @overload + def create(self, body: WebhooksCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def create(self, body: WebhooksCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def create(self, body: WebhooksCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Register a webhook endpoint for one organization or the whole environment. Returns its signing secret once. + + Requires scope `webhooks:write`. Danger: critical. + + ``POST /webhooks`` · action ``webhooks.create`` + Scope ``webhooks:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Delete a webhook endpoint. It stops receiving events at once. + + Requires scope `webhooks:write`. Danger: destructive. + + ``DELETE /webhooks/{id}`` · action ``webhooks.delete`` + Scope ``webhooks:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Get one webhook endpoint: URL, owner, subscribed events, health. Never its signing secret. + + Requires scope `webhooks:read`. Danger: read. + + ``GET /webhooks/{id}`` · action ``webhooks.get`` + Scope ``webhooks:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: WebhooksListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WebhooksListData]: ... + + @overload + def list(self, query: WebhooksListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WebhooksListData] | PendingApprovalResult[WebhooksListData]: ... + + def list(self, query: WebhooksListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WebhooksListData] | PendingApprovalResult[WebhooksListData]: + """List the webhook endpoints registered in this environment: URL, owner, subscribed events and whether each is active. + + Requires scope `webhooks:read`. Danger: read. + + ``GET /webhooks`` · action ``webhooks.list`` + Scope ``webhooks:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: WebhooksListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Webhook]: + """Every item of ``webhooks.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(ENVIRONMENT_OPERATIONS["webhooks.list"], (), query, headers=headers) + + @overload + def pause(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def pause(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def pause(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Pause a webhook endpoint: it stops receiving events until resumed. + + Requires scope `webhooks:write`. Danger: write. + + ``POST /webhooks/{id}/pause`` · action ``webhooks.pause`` + Scope ``webhooks:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.pause"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def resume(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def resume(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def resume(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Resume a paused webhook endpoint: it receives events again. + + Requires scope `webhooks:write`. Danger: write. + + ``POST /webhooks/{id}/resume`` · action ``webhooks.resume`` + Scope ``webhooks:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.resume"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def update(self, id: str, body: WebhooksUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def update(self, id: str, body: WebhooksUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def update(self, id: str, body: WebhooksUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Change a webhook endpoint's URL and/or the events it subscribes to. + + Requires scope `webhooks:write`. Danger: write. + + ``PATCH /webhooks/{id}`` · action ``webhooks.update`` + Scope ``webhooks:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +# ── Client ──────────────────────────────────────────────────────────────────────────────────────── + + +class EnvironmentClient(ManagementClient): + """Cbox ID — Environment Management API. + + The **per-environment management plane** — the API an app's own backend uses to run + its tenancy inside ONE environment: organizations and their owners, members, + invitations, roles (in one organization, or everywhere for your staff), apps, APIs, + the API keys customers made for them, and support sessions. It is served on + the environment's own host ({slug}.cboxid.com or a custom domain): the host pins + the environment, and the request is authenticated with an environment API key + (`cbid_env_…`). + + ``base_url`` is required: the environment's own host. + """ + + _plane = "environment" + + def _bind(self, core: ManagementTransport) -> None: + self.access_reviews = AccessReviewsMethods(core) + self.action_approvals = ActionApprovalsMethods(core) + self.api_keys = ApiKeysMethods(core) + self.apis = ApisMethods(core) + self.approvals = ApprovalsMethods(core) + self.apps = AppsMethods(core) + self.audit = AuditMethods(core) + self.audit_logs = AuditLogsMethods(core) + self.branding = BrandingMethods(core) + self.directories = DirectoriesMethods(core) + self.domains = DomainsMethods(core) + self.events = EventsMethods(core) + self.frontend_keys = FrontendKeysMethods(core) + self.hooks = HooksMethods(core) + self.invitations = InvitationsMethods(core) + self.keys = KeysMethods(core) + self.legacy_login = LegacyLoginMethods(core) + self.log_streams = LogStreamsMethods(core) + self.members = MembersMethods(core) + self.organizations = OrganizationsMethods(core) + self.permissions = PermissionsMethods(core) + self.provisioning = ProvisioningMethods(core) + self.roles = RolesMethods(core) + self.saml_apps = SamlAppsMethods(core) + self.signin = SigninMethods(core) + self.sod_policies = SodPoliciesMethods(core) + self.sso = SsoMethods(core) + self.support_sessions = SupportSessionsMethods(core) + self.token_vault = TokenVaultMethods(core) + self.users = UsersMethods(core) + self.webhooks = WebhooksMethods(core) diff --git a/src/cbox_id/management/generated/platform.py b/src/cbox_id/management/generated/platform.py new file mode 100644 index 0000000..7af82ac --- /dev/null +++ b/src/cbox_id/management/generated/platform.py @@ -0,0 +1,408 @@ +# GENERATED by scripts/generate_management.py from openapi/platform.yaml — do not edit. +# Regenerate with `python -m scripts.generate_management`. +"""Cbox ID — Operator API: typed client, schemas and operation table.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Literal, overload + +from typing_extensions import NotRequired, TypedDict + +from ..client import ManagementClient +from ..models import ApiResponse, ApprovalMode, OperationSpec, PendingApprovalResult +from ..transport import ManagementTransport + +# ── Schemas (components.schemas) ────────────────────────────────────────────────────────────────── + + +class ProvisionedWorkspaceOwner(TypedDict): + id: str + email: str + + +class ProvisionedWorkspace(TypedDict): + id: str + name: str + owner: ProvisionedWorkspaceOwner + + #: The workspace's first project. + project_id: str + + #: That project's first environment. + environment_id: str + + #: Whether the owner was emailed a link to set their password. + owner_invited: bool + + +class PlatformOrganization(TypedDict): + id: str + name: str + slug: str + type: Literal["customer", "reseller"] + status: Literal["active", "suspended", "deleted"] + parent_id: NotRequired[str | None] + + #: The environment it lives in. + environment_id: NotRequired[str | None] + created_at: NotRequired[str | None] + + +class PlatformEnvironment(TypedDict): + id: str + name: str + slug: str + + #: The verified domain it serves on — null until one is verified by DNS. + domain: NotRequired[str | None] + status: str + + #: The project that owns it; null for one no customer owns yet. + project_id: NotRequired[str | None] + created_at: NotRequired[str | None] + + +class ProvisionedEnvironmentAdmin(TypedDict): + id: str + email: str + + +class ProvisionedEnvironment(TypedDict): + environment_id: str + organization: PlatformOrganization + admin: ProvisionedEnvironmentAdmin + + +class PlatformOperator(TypedDict): + id: str + email: str + name: NotRequired[str | None] + status: Literal["active", "suspended"] + created_at: NotRequired[str | None] + + +class Error(TypedDict): + #: Stable machine code + error: str + + #: Human-readable explanation + message: str + + #: The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem. + request_id: NotRequired[str] + + #: Present on `validation_failed` only — the offending fields, each mapped to its messages. + errors: NotRequired[dict[str, list[str]]] + + +# ── Operation inputs and responses ──────────────────────────────────────────────────────────────── + + +class ActionApprovalsGetData(TypedDict): + id: str + status: Literal["pending", "approved", "denied", "expired", "consumed"] + + +class EnvironmentsCreateBody(TypedDict): + name: str + + #: The domain it will serve on, once verified by DNS. Checked to be free; not written yet. + domain: NotRequired[str | None] + + +class EnvironmentsProvisionBody(TypedDict): + organization_name: str + admin_name: str + admin_email: str + + #: Write-only. Must meet the environment's own password policy. + admin_password: str + + +class OperatorsCreateBody(TypedDict): + name: str + email: str + + #: Write-only. At least 12 characters, and not one known from a breach. + password: str + + +class OperatorsSetStatusBody(TypedDict): + status: Literal["active", "suspended"] + + +class OrganizationsCreateBody(TypedDict): + name: str + type: Literal["customer", "reseller"] + + #: An organization of the same environment to create it under. + parent_id: NotRequired[str | None] + + +class OrganizationsMoveBody(TypedDict): + #: The new parent, of the same environment; null for the top of the hierarchy. + parent_id: NotRequired[str | None] + + +class OrganizationsSetStatusBody(TypedDict): + status: Literal["active", "suspended"] + + +class WorkspacesCreateBody(TypedDict): + #: The workspace's name — usually the company it belongs to. + name: str + + #: The owner. An address that already has an account keeps it; the workspace is added to it. + owner_email: str + owner_name: str + + #: The first project's environment allowance: 1, 2, 3, 5, 10 or 25. + environment_limit: int + + +class WorkspacesSetStatusBody(TypedDict): + status: Literal["active", "suspended"] + + +# ── Operations ──────────────────────────────────────────────────────────────────────────────────── + + +#: Every operation of the Cbox ID — Operator API, keyed by action name. +PLATFORM_OPERATIONS: Mapping[str, OperationSpec] = { + "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/platform/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), + "platform.environments.create": OperationSpec(action="platform.environments.create", operation_id="platform_environments_create", method="POST", path="/platform/environments", path_params=(), scope="operator:environments:write", danger="critical", approval=True, body=True, pagination=None), + "platform.environments.provision": OperationSpec(action="platform.environments.provision", operation_id="platform_environments_provision", method="POST", path="/platform/environments/{environment_id}/provision", path_params=("environment_id",), scope="operator:environments:write", danger="critical", approval=True, body=True, pagination=None), + "platform.operators.create": OperationSpec(action="platform.operators.create", operation_id="platform_operators_create", method="POST", path="/platform/operators", path_params=(), scope="operator:operators:write", danger="critical", approval=True, body=True, pagination=None), + "platform.operators.set_status": OperationSpec(action="platform.operators.set_status", operation_id="platform_operators_set_status", method="PUT", path="/platform/operators/{operator_id}/status", path_params=("operator_id",), scope="operator:operators:write", danger="critical", approval=True, body=True, pagination=None), + "platform.organizations.create": OperationSpec(action="platform.organizations.create", operation_id="platform_organizations_create", method="POST", path="/platform/environments/{environment_id}/organizations", path_params=("environment_id",), scope="operator:organizations:write", danger="critical", approval=True, body=True, pagination=None), + "platform.organizations.move": OperationSpec(action="platform.organizations.move", operation_id="platform_organizations_move", method="PUT", path="/platform/environments/{environment_id}/organizations/{organization_id}/parent", path_params=("environment_id", "organization_id"), scope="operator:organizations:write", danger="critical", approval=True, body=True, pagination=None), + "platform.organizations.set_status": OperationSpec(action="platform.organizations.set_status", operation_id="platform_organizations_set_status", method="PUT", path="/platform/environments/{environment_id}/organizations/{organization_id}/status", path_params=("environment_id", "organization_id"), scope="operator:organizations:write", danger="critical", approval=True, body=True, pagination=None), + "platform.workspaces.create": OperationSpec(action="platform.workspaces.create", operation_id="platform_workspaces_create", method="POST", path="/platform/workspaces", path_params=(), scope="operator:workspaces:write", danger="critical", approval=True, body=True, pagination=None), + "platform.workspaces.set_status": OperationSpec(action="platform.workspaces.set_status", operation_id="platform_workspaces_set_status", method="PUT", path="/platform/workspaces/{workspace_id}/status", path_params=("workspace_id",), scope="operator:workspaces:write", danger="critical", approval=True, body=True, pagination=None), +} + + +# ── Namespaces ──────────────────────────────────────────────────────────────────────────────────── + + +class ActionApprovalsMethods: + """``action_approvals.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + def get(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ActionApprovalsGetData]: + """Where an approval you asked for stands + + The `poll_url` of a `202 approval_required`: an approval this token raised, and only those — another token's id is a 404. `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + + ``GET /platform/action-approvals/{id}`` + """ + return self._core.call(PLATFORM_OPERATIONS["action_approvals.get"], (id,), None, headers=headers) + + +class EnvironmentsMethods: + """``environments.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: EnvironmentsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformEnvironment]: ... + + @overload + def create(self, body: EnvironmentsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformEnvironment] | PendingApprovalResult[PlatformEnvironment]: ... + + def create(self, body: EnvironmentsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformEnvironment] | PendingApprovalResult[PlatformEnvironment]: + """Create an environment on the deployment, with its own signing key. A domain is verified separately, by DNS. + + Requires scope `operator:environments:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``POST /platform/environments`` · action ``platform.environments.create`` + Scope ``operator:environments:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.environments.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def provision(self, environment_id: str, body: EnvironmentsProvisionBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisionedEnvironment]: ... + + @overload + def provision(self, environment_id: str, body: EnvironmentsProvisionBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisionedEnvironment] | PendingApprovalResult[ProvisionedEnvironment]: ... + + def provision(self, environment_id: str, body: EnvironmentsProvisionBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisionedEnvironment] | PendingApprovalResult[ProvisionedEnvironment]: + """Bootstrap an environment with its first organization and an owner administrator, so people can sign in to it. + + Requires scope `operator:environments:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``POST /platform/environments/{environment_id}/provision`` · action ``platform.environments.provision`` + Scope ``operator:environments:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.environments.provision"], (environment_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class OperatorsMethods: + """``operators.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: OperatorsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOperator]: ... + + @overload + def create(self, body: OperatorsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOperator] | PendingApprovalResult[PlatformOperator]: ... + + def create(self, body: OperatorsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOperator] | PendingApprovalResult[PlatformOperator]: + """Add a platform operator: a person with authority over the whole deployment. + + Requires scope `operator:operators:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``POST /platform/operators`` · action ``platform.operators.create`` + Scope ``operator:operators:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.operators.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def set_status(self, operator_id: str, body: OperatorsSetStatusBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOperator]: ... + + @overload + def set_status(self, operator_id: str, body: OperatorsSetStatusBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOperator] | PendingApprovalResult[PlatformOperator]: ... + + def set_status(self, operator_id: str, body: OperatorsSetStatusBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOperator] | PendingApprovalResult[PlatformOperator]: + """Suspend a platform operator, or reactivate one. Never yourself, and never the last active operator. + + Requires scope `operator:operators:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``PUT /platform/operators/{operator_id}/status`` · action ``platform.operators.set_status`` + Scope ``operator:operators:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.operators.set_status"], (operator_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class OrganizationsMethods: + """``organizations.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, environment_id: str, body: OrganizationsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization]: ... + + @overload + def create(self, environment_id: str, body: OrganizationsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: ... + + def create(self, environment_id: str, body: OrganizationsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: + """Create an organization inside an environment, optionally under a parent organization of the same environment. + + Requires scope `operator:organizations:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``POST /platform/environments/{environment_id}/organizations`` · action ``platform.organizations.create`` + Scope ``operator:organizations:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.organizations.create"], (environment_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def move(self, environment_id: str, organization_id: str, body: OrganizationsMoveBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization]: ... + + @overload + def move(self, environment_id: str, organization_id: str, body: OrganizationsMoveBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: ... + + def move(self, environment_id: str, organization_id: str, body: OrganizationsMoveBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: + """Move an organization under another organization of the same environment, or to the top of its hierarchy. + + Requires scope `operator:organizations:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``PUT /platform/environments/{environment_id}/organizations/{organization_id}/parent`` · action ``platform.organizations.move`` + Scope ``operator:organizations:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.organizations.move"], (environment_id, organization_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def set_status(self, environment_id: str, organization_id: str, body: OrganizationsSetStatusBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization]: ... + + @overload + def set_status(self, environment_id: str, organization_id: str, body: OrganizationsSetStatusBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: ... + + def set_status(self, environment_id: str, organization_id: str, body: OrganizationsSetStatusBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: + """Suspend an organization inside an environment (its members can no longer sign in to it) or reactivate it. + + Requires scope `operator:organizations:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``PUT /platform/environments/{environment_id}/organizations/{organization_id}/status`` · action ``platform.organizations.set_status`` + Scope ``operator:organizations:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.organizations.set_status"], (environment_id, organization_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class WorkspacesMethods: + """``workspaces.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: WorkspacesCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisionedWorkspace]: ... + + @overload + def create(self, body: WorkspacesCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisionedWorkspace] | PendingApprovalResult[ProvisionedWorkspace]: ... + + def create(self, body: WorkspacesCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProvisionedWorkspace] | PendingApprovalResult[ProvisionedWorkspace]: + """Create a workspace with its owner, first project and first environment; the owner is emailed a link to set their password. + + Requires scope `operator:workspaces:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``POST /platform/workspaces`` · action ``platform.workspaces.create`` + Scope ``operator:workspaces:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.workspaces.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def set_status(self, workspace_id: str, body: WorkspacesSetStatusBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization]: ... + + @overload + def set_status(self, workspace_id: str, body: WorkspacesSetStatusBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: ... + + def set_status(self, workspace_id: str, body: WorkspacesSetStatusBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization]: + """Suspend a workspace (its people can no longer sign in, its environments stop serving) or reactivate it. + + Requires scope `operator:workspaces:write` on an access token delegated by an active platform operator. No management key is accepted. Danger: critical. + + ``PUT /platform/workspaces/{workspace_id}/status`` · action ``platform.workspaces.set_status`` + Scope ``operator:workspaces:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(PLATFORM_OPERATIONS["platform.workspaces.set_status"], (workspace_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +# ── Client ──────────────────────────────────────────────────────────────────────────────────────── + + +class PlatformClient(ManagementClient): + """Cbox ID — Operator API. + + The **deployment itself**, for the staff who run it: stand up customer workspaces, + create and bootstrap environments, manage organizations inside any environment, and + keep the operator roster. Everything the console's Platform pages do, as the same + actions — the same rules, refusals and audit trail. + + ``base_url`` defaults to ``https://api.cboxid.com``. + """ + + _plane = "platform" + _default_base_url = "https://api.cboxid.com" + + def _bind(self, core: ManagementTransport) -> None: + self.action_approvals = ActionApprovalsMethods(core) + self.environments = EnvironmentsMethods(core) + self.operators = OperatorsMethods(core) + self.organizations = OrganizationsMethods(core) + self.workspaces = WorkspacesMethods(core) diff --git a/src/cbox_id/management/generated/workspace.py b/src/cbox_id/management/generated/workspace.py new file mode 100644 index 0000000..8acbc4b --- /dev/null +++ b/src/cbox_id/management/generated/workspace.py @@ -0,0 +1,861 @@ +# GENERATED by scripts/generate_management.py from openapi/workspace.yaml — do not edit. +# Regenerate with `python -m scripts.generate_management`. +"""Cbox ID — Workspace Management API: typed client, schemas and operation table.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from typing import Any, Literal, TypeAlias, overload + +from typing_extensions import NotRequired, TypedDict + +from ..client import ManagementClient +from ..models import ApiResponse, ApprovalMode, OperationSpec, PendingApprovalResult +from ..transport import ManagementTransport + +# ── Schemas (components.schemas) ────────────────────────────────────────────────────────────────── + + +class Organization(TypedDict): + id: NotRequired[str] + name: NotRequired[str] + status: NotRequired[Literal["active", "suspended"]] + + #: The organization's projects with each one's plan/allowance. Present only for billing-readers (plans are per project). + projects: NotRequired[list[Project]] + + +class Project(TypedDict): + id: NotRequired[str] + name: NotRequired[str] + slug: NotRequired[str] + status: NotRequired[Literal["active", "suspended"]] + + #: This project's plan environment allowance. + environment_limit: NotRequired[int] + environments_used: NotRequired[int] + + +class Environment(TypedDict): + id: NotRequired[str] + name: NotRequired[str] + slug: NotRequired[str] + type: NotRequired[Literal["production", "sandbox"]] + status: NotRequired[str] + + #: The project (billing anchor) this environment belongs to. + project_id: NotRequired[str] + domain: NotRequired[str | None] + + #: The environment's own host — where its OIDC/environment API lives. + issuer: NotRequired[str] + + +class Member(TypedDict): + id: NotRequired[str] + email: NotRequired[str] + name: NotRequired[str | None] + role: NotRequired[Literal["owner", "admin", "developer", "member", "viewer"]] + status: NotRequired[Literal["active", "invited"]] + all_environments: NotRequired[bool] + + +class CreatedEnvironment(Environment): + """An environment, and — when the request asked for one — its first management key.""" + + initial_key: NotRequired[EnvironmentKey] + + +class EnvironmentKey(TypedDict): + """A management key (`cbid_env_…`) of one of the workspace's environments. `token` is present only in the answer that minted it, and `null` on an idempotent replay.""" + + id: str + environment_id: str + name: str + prefix: NotRequired[str] + scopes: list[str] + expires_at: NotRequired[str | None] + + #: The key's value. Shown once. + token: NotRequired[str | None] + + +class WorkspaceKey(TypedDict): + """A workspace key (`cbid_ws_…`). `token` is present only in the answer that minted it, and `null` on an idempotent replay.""" + + id: str + name: str + prefix: NotRequired[str] + role: Literal["owner", "admin", "developer", "member", "viewer"] + + #: Null when the role alone bounds the key. + scopes: NotRequired[list[Literal["workspace:read", "projects:write", "environments:write", "team:read", "team:write", "keys:write", "settings:write"]] | None] + + #: The key that minted it, if a key did. Revoking the parent revokes it. + parent_key_id: NotRequired[str | None] + + #: `organization_member` or `workspace_key`; null for a key minted before this was recorded. + created_by_type: NotRequired[str | None] + expires_at: NotRequired[str | None] + revoked_at: NotRequired[str | None] + last_used_at: NotRequired[str | None] + + #: The key's value. Shown once. + token: NotRequired[str | None] + + +class TeamInvitation(TypedDict): + id: str + email: str + role: Literal["admin", "developer", "member", "viewer"] + + #: The person, or the API key, that sent it — by name. + invited_by: NotRequired[str | None] + invited_at: NotRequired[str | None] + expires_at: str + + +class CustomDomainPendingOption1(TypedDict): + domain: str + record_name: str + record_value: str + + +class CustomDomain(TypedDict): + #: The verified domain serving the environment. + domain: str | None + verified_at: str | None + + #: A domain waiting for its DNS proof, and the TXT record to publish. + pending: CustomDomainPendingOption1 | None + + +class PageMeta(TypedDict): + """Paging for a list response. `has_more` used to be returned with nothing to act on it — no page parameter and no cursor — so a caller told there was more had no way to ask for it. `next_page` is present exactly when `has_more` is true, so a client can loop without computing anything.""" + + limit: NotRequired[int] + page: NotRequired[int] + + #: Present on lists that can count cheaply. + total: NotRequired[int] + has_more: NotRequired[bool] + + #: Pass as `page` on the next request; absent on the last page. + next_page: NotRequired[int | None] + + +class Error(TypedDict): + #: Stable machine code + error: str + + #: Human-readable explanation + message: str + + #: The id this request was served under, also sent as the X-Request-Id header. Quote it when reporting a problem. + request_id: NotRequired[str] + + #: Present on `validation_failed` only: the offending request fields, each + #: mapped to its messages. + errors: NotRequired[dict[str, list[str]]] + + +# ── Operation inputs and responses ──────────────────────────────────────────────────────────────── + + +class ActionApprovalsGetData(TypedDict): + id: str + status: Literal["pending", "approved", "denied", "expired", "consumed"] + + +class EnvironmentsCreateBodyInitialKey(TypedDict): + """Mint the environment's first management key in the same call.""" + + name: str + + #: Environment-key scopes, for example `apis:write`. + scopes: list[str] + expires_at: NotRequired[str | None] + + +class EnvironmentsCreateBody(TypedDict): + name: str + type: NotRequired[Literal["production", "sandbox"]] + + #: The project (billing anchor) to create the environment under. Defaults to the organization's first project when omitted. + project_id: NotRequired[str] + + #: Mint the environment's first management key in the same call. + initial_key: NotRequired[EnvironmentsCreateBodyInitialKey | None] + + +class EnvironmentsDomainRequestBody(TypedDict): + #: The domain, for example login.example.com. + domain: str + + +class EnvironmentsListQuery(TypedDict): + limit: NotRequired[int] + page: NotRequired[int] + + +EnvironmentsListData: TypeAlias = 'list[Environment]' + + +class KeysEnvironmentCreateBody(TypedDict): + name: str + + #: The environment-key scopes it carries, for example `apis:write`. + scopes: list[str] + + #: When it stops working. Left out, it does not expire. + expires_at: NotRequired[str | None] + + +class KeysWorkspaceCreateBody(TypedDict): + name: str + role: Literal["admin", "developer", "member", "viewer"] + + #: Narrow the key below its role. Left out, the role alone bounds it — or, minted by a key with scopes, that key's scopes. + scopes: NotRequired[list[Literal["workspace:read", "projects:write", "environments:write", "team:read", "team:write", "keys:write", "settings:write"]] | None] + + #: When it stops working. Left out, it does not expire — or, minted by a key that does, expires with it. + expires_at: NotRequired[str | None] + + +class KeysWorkspaceListQuery(TypedDict): + #: Items per page, 1–100. Default 50. + limit: NotRequired[int] + + #: The page to read, from 1. A list says `next_page` when there is one. + page: NotRequired[int] + + +KeysWorkspaceListData: TypeAlias = 'list[WorkspaceKey]' + + +class ProjectsCreateBody(TypedDict): + name: str + + #: This project's plan environment allowance (default 2). + environment_limit: NotRequired[int] + + +ProjectsListData: TypeAlias = 'list[Project]' + + +class ProjectsRenameBody(TypedDict): + name: str + + +ProjectsVerificationResendData: TypeAlias = dict[str, Any] + + +class TeamEnvironmentAccessBody(TypedDict): + #: True: every environment, including ones created later. + all_environments: bool + + #: With `all_environments` false: the environments they reach. + environment_ids: NotRequired[list[str]] + + +TeamInvitationsListData: TypeAlias = 'list[TeamInvitation]' + + +class TeamInviteBody(TypedDict): + email: str + name: NotRequired[str | None] + role: Literal["admin", "developer", "member", "viewer"] + + +class TeamListQuery(TypedDict): + limit: NotRequired[int] + page: NotRequired[int] + + +TeamListData: TypeAlias = 'list[Member]' + + +class TeamRoleBody(TypedDict): + role: Literal["admin", "developer", "member", "viewer"] + + +class WorkspaceSettingsUpdateBody(TypedDict): + name: str + + +# ── Operations ──────────────────────────────────────────────────────────────────────────────────── + + +#: Every operation of the Cbox ID — Workspace Management API, keyed by action name. +WORKSPACE_OPERATIONS: Mapping[str, OperationSpec] = { + "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/workspace/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), + "environments.create": OperationSpec(action="environments.create", operation_id="environments_create", method="POST", path="/workspace/environments", path_params=(), scope="environments:write", danger="critical", approval=False, body=True, pagination=None), + "environments.domain.remove": OperationSpec(action="environments.domain.remove", operation_id="environments_domain_remove", method="DELETE", path="/workspace/environments/{environment_id}/domain", path_params=("environment_id",), scope="environments:write", danger="destructive", approval=True, body=False, pagination=None), + "environments.domain.request": OperationSpec(action="environments.domain.request", operation_id="environments_domain_request", method="POST", path="/workspace/environments/{environment_id}/domain", path_params=("environment_id",), scope="environments:write", danger="write", approval=True, body=True, pagination=None), + "environments.domain.verify": OperationSpec(action="environments.domain.verify", operation_id="environments_domain_verify", method="POST", path="/workspace/environments/{environment_id}/domain/verify", path_params=("environment_id",), scope="environments:write", danger="write", approval=True, body=False, pagination=None), + "environments.list": OperationSpec(action="environments.list", operation_id="environments_list", method="GET", path="/workspace/environments", path_params=(), scope="workspace:read", danger=None, approval=False, body=False, pagination="page"), + "keys.environment.create": OperationSpec(action="keys.environment.create", operation_id="keys_environment_create", method="POST", path="/workspace/environments/{environment_id}/keys", path_params=("environment_id",), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), + "keys.environment.revoke": OperationSpec(action="keys.environment.revoke", operation_id="keys_environment_revoke", method="DELETE", path="/workspace/environments/{environment_id}/keys/{id}", path_params=("environment_id", "id"), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), + "keys.workspace.create": OperationSpec(action="keys.workspace.create", operation_id="keys_workspace_create", method="POST", path="/workspace/keys", path_params=(), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), + "keys.workspace.list": OperationSpec(action="keys.workspace.list", operation_id="keys_workspace_list", method="GET", path="/workspace/keys", path_params=(), scope="workspace:read", danger="read", approval=True, body=False, pagination="page"), + "keys.workspace.revoke": OperationSpec(action="keys.workspace.revoke", operation_id="keys_workspace_revoke", method="DELETE", path="/workspace/keys/{id}", path_params=("id",), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), + "projects.create": OperationSpec(action="projects.create", operation_id="projects_create", method="POST", path="/workspace/projects", path_params=(), scope="projects:write", danger=None, approval=False, body=True, pagination=None), + "projects.list": OperationSpec(action="projects.list", operation_id="projects_list", method="GET", path="/workspace/projects", path_params=(), scope="workspace:read", danger=None, approval=False, body=False, pagination=None), + "projects.reactivate": OperationSpec(action="projects.reactivate", operation_id="projects_reactivate", method="POST", path="/workspace/projects/{id}/reactivate", path_params=("id",), scope="projects:write", danger="write", approval=True, body=False, pagination=None), + "projects.rename": OperationSpec(action="projects.rename", operation_id="projects_rename", method="PATCH", path="/workspace/projects/{id}", path_params=("id",), scope="projects:write", danger="write", approval=True, body=True, pagination=None), + "projects.suspend": OperationSpec(action="projects.suspend", operation_id="projects_suspend", method="POST", path="/workspace/projects/{id}/suspend", path_params=("id",), scope="projects:write", danger="write", approval=True, body=False, pagination=None), + "projects.verification.resend": OperationSpec(action="projects.verification.resend", operation_id="projects_verification_resend", method="POST", path="/workspace/projects/verification/resend", path_params=(), scope="projects:write", danger="write", approval=True, body=False, pagination=None), + "team.environment_access": OperationSpec(action="team.environment_access", operation_id="team_environment_access", method="PUT", path="/workspace/members/{id}/access", path_params=("id",), scope="team:write", danger="write", approval=True, body=True, pagination=None), + "team.invitations.list": OperationSpec(action="team.invitations.list", operation_id="team_invitations_list", method="GET", path="/workspace/invitations", path_params=(), scope="team:read", danger=None, approval=False, body=False, pagination=None), + "team.invitations.resend": OperationSpec(action="team.invitations.resend", operation_id="team_invitations_resend", method="POST", path="/workspace/invitations/{id}/resend", path_params=("id",), scope="team:write", danger=None, approval=False, body=False, pagination=None), + "team.invitations.revoke": OperationSpec(action="team.invitations.revoke", operation_id="team_invitations_revoke", method="DELETE", path="/workspace/invitations/{id}", path_params=("id",), scope="team:write", danger=None, approval=False, body=False, pagination=None), + "team.invite": OperationSpec(action="team.invite", operation_id="team_invite", method="POST", path="/workspace/members", path_params=(), scope="team:write", danger=None, approval=False, body=True, pagination=None), + "team.list": OperationSpec(action="team.list", operation_id="team_list", method="GET", path="/workspace/members", path_params=(), scope="team:read", danger=None, approval=False, body=False, pagination="page"), + "team.remove": OperationSpec(action="team.remove", operation_id="team_remove", method="DELETE", path="/workspace/members/{id}", path_params=("id",), scope="team:write", danger="destructive", approval=True, body=False, pagination=None), + "team.role": OperationSpec(action="team.role", operation_id="team_role", method="PATCH", path="/workspace/members/{id}/role", path_params=("id",), scope="team:write", danger="write", approval=True, body=True, pagination=None), + "team.transfer_ownership": OperationSpec(action="team.transfer_ownership", operation_id="team_transfer_ownership", method="POST", path="/workspace/members/{id}/transfer-ownership", path_params=("id",), scope="team:write", danger="critical", approval=True, body=False, pagination=None), + "workspace.get": OperationSpec(action="workspace.get", operation_id="workspace_get", method="GET", path="/workspace", path_params=(), scope="workspace:read", danger=None, approval=False, body=False, pagination=None), + "workspace.settings.update": OperationSpec(action="workspace.settings.update", operation_id="workspace_settings_update", method="PATCH", path="/workspace", path_params=(), scope="settings:write", danger="write", approval=True, body=True, pagination=None), +} + + +# ── Namespaces ──────────────────────────────────────────────────────────────────────────────────── + + +class ActionApprovalsMethods: + """``action_approvals.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + def get(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ActionApprovalsGetData]: + """Where an approval this key asked for stands + + Any workspace key — or a member's token — may poll the approvals it raised, and only + those: another credential's id is a 404. `status` is `pending`, `approved`, `denied`, `expired` or `consumed`. + + ``GET /workspace/action-approvals/{id}`` + """ + return self._core.call(WORKSPACE_OPERATIONS["action_approvals.get"], (id,), None, headers=headers) + + +class EnvironmentsDomainMethods: + """``environments.domain.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def remove(self, environment_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, environment_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, environment_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Stop serving one of the workspace's environments on its custom domain. Anything using that domain stops reaching it. + + Requires scope `environments:write` and a role that may `manage-environments`. Danger: destructive. + + ``DELETE /workspace/environments/{environment_id}/domain`` · action ``environments.domain.remove`` + Scope ``environments:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["environments.domain.remove"], (environment_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def request(self, environment_id: str, body: EnvironmentsDomainRequestBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain]: ... + + @overload + def request(self, environment_id: str, body: EnvironmentsDomainRequestBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: ... + + def request(self, environment_id: str, body: EnvironmentsDomainRequestBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: + """Start serving one of the workspace's environments on a custom domain: returns the DNS TXT record that proves you control it. + + Requires scope `environments:write` and a role that may `manage-environments`. Danger: write. + + ``POST /workspace/environments/{environment_id}/domain`` · action ``environments.domain.request`` + Scope ``environments:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["environments.domain.request"], (environment_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def verify(self, environment_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain]: ... + + @overload + def verify(self, environment_id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: ... + + def verify(self, environment_id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain]: + """Look for the pending domain's DNS TXT record and, once it is visible, serve the environment on that domain. + + Requires scope `environments:write` and a role that may `manage-environments`. Danger: write. + + ``POST /workspace/environments/{environment_id}/domain/verify`` · action ``environments.domain.verify`` + Scope ``environments:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["environments.domain.verify"], (environment_id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class EnvironmentsMethods: + """``environments.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.domain = EnvironmentsDomainMethods(core) + + def create(self, body: EnvironmentsCreateBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CreatedEnvironment]: + """Create an environment + + Requires scope `environments:write` and the `manage-environments` capability + (owner/admin/developer). Danger: critical — with `initial_key` it mints a credential. + + With `initial_key`, the environment's first management key is minted in the same + call and returned once as `initial_key.token` — enough for an agent holding one + workspace key to stand an environment up and start configuring it on the + environment's own host. An idempotent replay returns `initial_key.token: null`. + + ``POST /workspace/environments`` · action ``environments.create`` + Scope ``environments:write`` · danger: critical. + """ + return self._core.call(WORKSPACE_OPERATIONS["environments.create"], (), body, idempotency_key=idempotency_key, headers=headers) + + def list(self, query: EnvironmentsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentsListData]: + """List environments + + Requires scope `workspace:read` (any role). + + ``GET /workspace/environments`` · action ``environments.list`` + Scope ``workspace:read``. + """ + return self._core.call(WORKSPACE_OPERATIONS["environments.list"], (), query, headers=headers) + + def list_all(self, query: EnvironmentsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Environment]: + """Every item of ``environments.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(WORKSPACE_OPERATIONS["environments.list"], (), query, headers=headers) + + +class KeysEnvironmentMethods: + """``keys.environment.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, environment_id: str, body: KeysEnvironmentCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentKey]: ... + + @overload + def create(self, environment_id: str, body: KeysEnvironmentCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentKey] | PendingApprovalResult[EnvironmentKey]: ... + + def create(self, environment_id: str, body: KeysEnvironmentCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentKey] | PendingApprovalResult[EnvironmentKey]: + """Mint a management key for one of the workspace's environments, with the scopes it needs. The value is returned once, as `token`. + + Requires scope `keys:write` and a role that may `manage-environments`. Danger: critical. + + ``POST /workspace/environments/{environment_id}/keys`` · action ``keys.environment.create`` + Scope ``keys:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["keys.environment.create"], (environment_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, environment_id: str, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, environment_id: str, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, environment_id: str, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke one of an environment's management keys; whatever uses it stops immediately. + + Requires scope `keys:write` and a role that may `manage-environments`. Danger: destructive. + + ``DELETE /workspace/environments/{environment_id}/keys/{id}`` · action ``keys.environment.revoke`` + Scope ``keys:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["keys.environment.revoke"], (environment_id, id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class KeysWorkspaceMethods: + """``keys.workspace.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def create(self, body: KeysWorkspaceCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WorkspaceKey]: ... + + @overload + def create(self, body: KeysWorkspaceCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WorkspaceKey] | PendingApprovalResult[WorkspaceKey]: ... + + def create(self, body: KeysWorkspaceCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[WorkspaceKey] | PendingApprovalResult[WorkspaceKey]: + """Mint a workspace key with a role and optional scopes, never wider than the caller. The value is returned once, as `token`. + + Requires scope `keys:write` and a role that may `manage-environments` and `manage-members`. Danger: critical. + + ``POST /workspace/keys`` · action ``keys.workspace.create`` + Scope ``keys:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["keys.workspace.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: KeysWorkspaceListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysWorkspaceListData]: ... + + @overload + def list(self, query: KeysWorkspaceListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysWorkspaceListData] | PendingApprovalResult[KeysWorkspaceListData]: ... + + def list(self, query: KeysWorkspaceListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysWorkspaceListData] | PendingApprovalResult[KeysWorkspaceListData]: + """List the workspace's keys, newest first, revoked ones included — names, roles, scopes and expiry, never their values. + + Requires scope `workspace:read` and a role that may `manage-members`. Danger: read. + + ``GET /workspace/keys`` · action ``keys.workspace.list`` + Scope ``workspace:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["keys.workspace.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) + + def list_all(self, query: KeysWorkspaceListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[WorkspaceKey]: + """Every item of ``keys.workspace.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(WORKSPACE_OPERATIONS["keys.workspace.list"], (), query, headers=headers) + + @overload + def revoke(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Revoke a workspace key, and every key it minted; whatever uses them stops immediately. + + Requires scope `keys:write` and a role that may `manage-environments` and `manage-members`. Danger: destructive. + + ``DELETE /workspace/keys/{id}`` · action ``keys.workspace.revoke`` + Scope ``keys:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["keys.workspace.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class KeysMethods: + """``keys.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.environment = KeysEnvironmentMethods(core) + self.workspace = KeysWorkspaceMethods(core) + + +class ProjectsVerificationMethods: + """``projects.verification.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def resend(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsVerificationResendData]: ... + + @overload + def resend(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsVerificationResendData] | PendingApprovalResult[ProjectsVerificationResendData]: ... + + def resend(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsVerificationResendData] | PendingApprovalResult[ProjectsVerificationResendData]: + """Re-send the signup confirmation the workspace's first environment is waiting on, to the owner's address on file. + + Requires scope `projects:write` and a role that may `manage-environments`. Danger: write. + + ``POST /workspace/projects/verification/resend`` · action ``projects.verification.resend`` + Scope ``projects:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["projects.verification.resend"], (), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class ProjectsMethods: + """``projects.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.verification = ProjectsVerificationMethods(core) + + def create(self, body: ProjectsCreateBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: + """Create a project + + Stand up another independently-billed IdP product. Requires scope `projects:write` and the `manage-environments` capability (owner/admin/developer). + + ``POST /workspace/projects`` · action ``projects.create`` + Scope ``projects:write``. + """ + return self._core.call(WORKSPACE_OPERATIONS["projects.create"], (), body, idempotency_key=idempotency_key, headers=headers) + + def list(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsListData]: + """List projects + + Requires scope `workspace:read` (any role). The organization's projects (IdP products). Each carries its own plan and environment allowance. + + ``GET /workspace/projects`` · action ``projects.list`` + Scope ``workspace:read``. + """ + return self._core.call(WORKSPACE_OPERATIONS["projects.list"], (), None, headers=headers) + + @overload + def reactivate(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: ... + + @overload + def reactivate(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: ... + + def reactivate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: + """Reactivate a suspended project, so environments can be added to it again. + + Requires scope `projects:write` and a role that may `manage-environments`. Danger: write. + + ``POST /workspace/projects/{id}/reactivate`` · action ``projects.reactivate`` + Scope ``projects:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["projects.reactivate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def rename(self, id: str, body: ProjectsRenameBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: ... + + @overload + def rename(self, id: str, body: ProjectsRenameBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: ... + + def rename(self, id: str, body: ProjectsRenameBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: + """Rename a project. + + Requires scope `projects:write` and a role that may `manage-environments`. Danger: write. + + ``PATCH /workspace/projects/{id}`` · action ``projects.rename`` + Scope ``projects:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["projects.rename"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def suspend(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: ... + + @overload + def suspend(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: ... + + def suspend(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: + """Suspend a project: its environments stay live, but no new ones can be added until it is reactivated. + + Requires scope `projects:write` and a role that may `manage-environments`. Danger: write. + + ``POST /workspace/projects/{id}/suspend`` · action ``projects.suspend`` + Scope ``projects:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["projects.suspend"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class TeamInvitationsMethods: + """``team.invitations.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + def list(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamInvitationsListData]: + """List pending invitations + + Requires scope `team:read` and the `read-members` capability. The team's invitations nobody has accepted yet, newest first (at most 100). + + ``GET /workspace/invitations`` · action ``team.invitations.list`` + Scope ``team:read``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.invitations.list"], (), None, headers=headers) + + def resend(self, id: str, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: + """Re-send an invitation + + Requires scope `team:write` and the `manage-members` capability. Mails a fresh link; the earlier link stops + working and the invitation gets a new `id`. At most once a minute per address + (`429`, `too_soon`). If the mail server refuses, the earlier invitation is kept (`503`). + + ``POST /workspace/invitations/{id}/resend`` · action ``team.invitations.resend`` + Scope ``team:write``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.invitations.resend"], (id,), None, idempotency_key=idempotency_key, headers=headers) + + def revoke(self, id: str, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: + """Withdraw an invitation + + Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending on this organization is a `404`. + + ``DELETE /workspace/invitations/{id}`` · action ``team.invitations.revoke`` + Scope ``team:write``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.invitations.revoke"], (id,), None, idempotency_key=idempotency_key, headers=headers) + + +class TeamMethods: + """``team.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.invitations = TeamInvitationsMethods(core) + + @overload + def environment_access(self, id: str, body: TeamEnvironmentAccessBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def environment_access(self, id: str, body: TeamEnvironmentAccessBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... + + def environment_access(self, id: str, body: TeamEnvironmentAccessBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: + """Set which of the workspace's environments a team member reaches: all of them, or the listed ones. + + Requires scope `team:write` and a role that may `manage-members`. Danger: write. + + ``PUT /workspace/members/{id}/access`` · action ``team.environment_access`` + Scope ``team:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.environment_access"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + def invite(self, body: TeamInviteBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: + """Invite a member + + Requires scope `team:write` and the `manage-members` capability (owner/admin). Invites the address onto the + organization's team — the same invitation the console's Team page sends: a mail naming + this key as the inviter and the role, with a signed link on which the invitee sets a + password and is signed in. Owner is never invited; ownership is transferred. An + earlier pending invitation for the same address is superseded. List, re-send and + withdraw it under `/workspace/invitations`. + + ``POST /workspace/members`` · action ``team.invite`` + Scope ``team:write``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.invite"], (), body, idempotency_key=idempotency_key, headers=headers) + + def list(self, query: TeamListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamListData]: + """List members + + Requires scope `team:read` and the `read-members` capability (owner/admin/viewer). The roster is PII — a developer key is refused. + + ``GET /workspace/members`` · action ``team.list`` + Scope ``team:read``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.list"], (), query, headers=headers) + + def list_all(self, query: TeamListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Member]: + """Every item of ``team.list``, fetching pages as the iteration reaches them.""" + return self._core.paginate(WORKSPACE_OPERATIONS["team.list"], (), query, headers=headers) + + @overload + def remove(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def remove(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def remove(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Remove a member from the workspace's team. The owner is never removed; transfer ownership first. + + Requires scope `team:write` and a role that may `manage-members`. Danger: destructive. + + ``DELETE /workspace/members/{id}`` · action ``team.remove`` + Scope ``team:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.remove"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def role(self, id: str, body: TeamRoleBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def role(self, id: str, body: TeamRoleBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... + + def role(self, id: str, body: TeamRoleBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: + """Change a team member's role (admin, developer, member or viewer). The owner's role changes only by transferring ownership. + + Requires scope `team:write` and a role that may `manage-members`. Danger: write. + + ``PATCH /workspace/members/{id}/role`` · action ``team.role`` + Scope ``team:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.role"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def transfer_ownership(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def transfer_ownership(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def transfer_ownership(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Make another member the workspace's owner; the current owner stays on as an admin. Only the owner, signed in, may do this. + + Requires scope `team:write` and a role that may `manage-members`. Danger: critical. + + ``POST /workspace/members/{id}/transfer-ownership`` · action ``team.transfer_ownership`` + Scope ``team:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["team.transfer_ownership"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class WorkspaceSettingsMethods: + """``workspace.settings.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def update(self, body: WorkspaceSettingsUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... + + @overload + def update(self, body: WorkspaceSettingsUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def update(self, body: WorkspaceSettingsUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Rename the workspace. + + Requires scope `settings:write` and a role that may `manage-members`. Danger: write. + + ``PATCH /workspace`` · action ``workspace.settings.update`` + Scope ``settings:write`` · danger: write. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(WORKSPACE_OPERATIONS["workspace.settings.update"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + +class WorkspaceMethods: + """``workspace.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + self.settings = WorkspaceSettingsMethods(core) + + def get(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: + """Get the organization + + Requires scope `workspace:read` (any role). Returns the organization's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer). + + ``GET /workspace`` · action ``workspace.get`` + Scope ``workspace:read``. + """ + return self._core.call(WORKSPACE_OPERATIONS["workspace.get"], (), None, headers=headers) + + +# ── Client ──────────────────────────────────────────────────────────────────────────────────────── + + +class WorkspaceClient(ManagementClient): + """Cbox ID — Workspace Management API. + + The **global organization-management plane** — above every environment. Manage the + workspace, its **projects** (IdP products, each a billing anchor) and their + environments, its team, and its keys with a workspace key. + + ``base_url`` defaults to ``https://api.cboxid.com``. + """ + + _plane = "workspace" + _default_base_url = "https://api.cboxid.com" + + def _bind(self, core: ManagementTransport) -> None: + self.action_approvals = ActionApprovalsMethods(core) + self.environments = EnvironmentsMethods(core) + self.keys = KeysMethods(core) + self.projects = ProjectsMethods(core) + self.team = TeamMethods(core) + self.workspace = WorkspaceMethods(core) diff --git a/src/cbox_id/management/models.py b/src/cbox_id/management/models.py new file mode 100644 index 0000000..5e9f0b6 --- /dev/null +++ b/src/cbox_id/management/models.py @@ -0,0 +1,135 @@ +"""Value types shared by every management plane: operation specs, responses, approvals.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any, Generic, Literal, TypeVar + +import httpx + +#: How much harm an action can do, as the server declares it (``x-danger``). +Danger = Literal["read", "write", "destructive", "critical"] + +#: How a list operation pages: an opaque ``after`` cursor, or a ``page`` number. +Pagination = Literal["cursor", "page"] + +#: The HTTP methods the management planes use. +HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"] + +#: ``"wait"`` (default): on ``202 approval_required``, call ``on_approval_required``, poll +#: until the person decides, and repeat the request. ``"return"``: hand the pending +#: approval back as a :class:`PendingApprovalResult` instead. +ApprovalMode = Literal["wait", "return"] + +#: The four management planes, each with its own OpenAPI document. +Plane = Literal["environment", "workspace", "platform", "account"] + +T = TypeVar("T") + + +@dataclass(frozen=True, slots=True) +class OperationSpec: + """What the generator knows about one operation. + + Exported per plane (``ENVIRONMENT_OPERATIONS`` …) so tooling can show an action's scope + and danger before running it. + """ + + #: The action name (``x-action``), e.g. ``apps.secrets.rotate``. ``None`` for a route + #: that is not an action. + action: str | None + operation_id: str | None + method: HttpMethod + #: Path relative to ``/api/v1``, with ``{param}`` placeholders. + path: str + #: Path parameter names, in the order the method takes them. + path_params: tuple[str, ...] + #: The scope the credential must carry, when the spec names one. + scope: str | None + danger: Danger | None + #: Whether the operation can answer ``202 approval_required``. + approval: bool + #: Whether the input travels as a JSON body (otherwise as the query string). + body: bool + pagination: Pagination | None + + +@dataclass(frozen=True, slots=True) +class PendingApproval: + """An approval a key's policy asked for — what a ``202 approval_required`` carries.""" + + id: str + status: str + #: Show this to the person: the same code appears on the device they approve on. + binding_code: str + expires_at: str + poll_url: str + + +@dataclass(frozen=True, slots=True) +class ApprovalContext: + """Handed to ``on_approval_required`` alongside the approval.""" + + #: The action held, e.g. ``apps.secrets.rotate``. + action: str | None + danger: Danger | None + method: str + path: str + + +@dataclass(frozen=True) +class ApiResponse(Generic[T]): + """A successful answer. ``data`` is the envelope's ``data``; ``body`` is the whole document.""" + + status: int + data: T + #: The envelope's ``meta`` (paging, on lists), when there is one. + meta: dict[str, Any] | None + body: Any + #: True when the server answered from its idempotency store (``Idempotent-Replayed: + #: true``): this is the FIRST request's answer, and any secret it carried (a client + #: secret, a key's token) is ``None`` — it was shown once, to the request that made it. + replayed: bool + #: The ``Idempotency-Key`` a write was sent with. + idempotency_key: str | None + #: The id the server served the request under (``X-Request-Id``). + request_id: str | None + headers: httpx.Headers = field(repr=False) + + @property + def pending(self) -> Literal[False]: + """Always ``False``: this call finished. See :class:`PendingApprovalResult`.""" + return False + + +@dataclass(frozen=True) +class PendingApprovalResult(Generic[T]): + """A write held for approval, returned instead of waited on (``approval="return"``). + + ``resume()`` polls until the person decides and repeats the request with the approval + and the same ``Idempotency-Key``. + """ + + approval: PendingApproval + idempotency_key: str | None + #: The action that was held, e.g. ``apps.secrets.rotate``. + action: str | None + _resume: Callable[[], ApiResponse[T]] = field(repr=False, compare=False) + + @property + def pending(self) -> Literal[True]: + """Always ``True``: the call is waiting for a person's approval.""" + return True + + @property + def status(self) -> Literal[202]: + return 202 + + def resume(self) -> ApiResponse[T]: + """Poll the approval until the person decides, then repeat the request. + + Raises :class:`~cbox_id.management.ApprovalDeniedError` or + :class:`~cbox_id.management.ApprovalExpiredError` when it is not approved. + """ + return self._resume() diff --git a/src/cbox_id/management/transport.py b/src/cbox_id/management/transport.py new file mode 100644 index 0000000..da807eb --- /dev/null +++ b/src/cbox_id/management/transport.py @@ -0,0 +1,798 @@ +"""The hand-written core every generated management client runs on. + +Authentication, idempotency keys, retries, the approval loop, error typing and pagination +live here. Generated code only describes operations; it never talks to the network itself. +""" + +from __future__ import annotations + +import json +import math +import random +import time +import uuid +from collections.abc import Callable, Iterator, Mapping, Sequence +from dataclasses import dataclass +from datetime import datetime, timezone +from email.utils import parsedate_to_datetime +from typing import Any, Literal, overload +from urllib.parse import quote, urljoin, urlsplit + +import httpx + +from ..errors import CboxIdError, ConfigurationError +from .dpop import DPoPSigner +from .errors import ( + ApprovalDeniedError, + ApprovalError, + ApprovalExpiredError, + CboxIdApiError, + ManagementNetworkError, +) +from .models import ( + ApiResponse, + ApprovalContext, + ApprovalMode, + HttpMethod, + OperationSpec, + PendingApproval, + PendingApprovalResult, + Plane, +) + +#: A delegated access token, or a callable returning one (called before every request, so +#: it can refresh). +AccessToken = str | Callable[[], str] + +#: Called when a write is held for a person's approval, before the client starts polling. +ApprovalHandler = Callable[[PendingApproval, ApprovalContext], None] + +_KEY_PREFIX: dict[str, str | None] = { + "environment": "cbid_env_", + "workspace": "cbid_ws_", + "platform": None, + "account": None, +} + +#: Where each plane serves ``GET …/action-approvals/{id}``, relative to ``/api/v1``. +_APPROVAL_MOUNT: dict[str, str] = { + "environment": "", + "workspace": "/workspace", + "platform": "/platform", + "account": "/me", +} + +_WRITE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"}) + +#: How many approvals one call will go through before giving up (a policy loop guard). +_MAX_APPROVAL_ROUNDS = 3 + +_LOOPBACK = frozenset({"localhost", "127.0.0.1", "::1"}) + + +@dataclass(frozen=True, slots=True) +class RetryOptions: + """Retries for network failures, ``5xx``, ``429`` and ``409 idempotency_in_progress``.""" + + #: Retries after the first attempt. ``0`` turns retrying off. + max_retries: int = 3 + #: First backoff delay (seconds), doubled per attempt with jitter. + base_delay: float = 0.5 + #: The longest the client waits before one retry (seconds). A ``Retry-After`` longer + #: than this is not waited out: the ``429``/``503`` is raised, with ``retry_after`` set. + max_delay: float = 30.0 + + +def retry_after_seconds(headers: httpx.Headers) -> float | None: + """``Retry-After`` in seconds, from delta-seconds or an HTTP date. + + ``None`` when the header is absent or unparseable. + """ + raw = headers.get("retry-after", "").strip() + + if raw == "": + return None + + if raw.isdigit(): + return float(raw) + + try: + when = parsedate_to_datetime(raw) + except (TypeError, ValueError): + return None + + if when.tzinfo is None: + when = when.replace(tzinfo=timezone.utc) + + return max(0.0, (when - datetime.now(timezone.utc)).total_seconds()) + + +def _normalize_base_url(raw: str) -> str: + parts = urlsplit(raw) + + if parts.scheme == "" or parts.hostname is None: + raise ConfigurationError("Management client `base_url` is not a valid URL.") + + loopback = parts.hostname in _LOOPBACK + + if parts.scheme != "https" and not (parts.scheme == "http" and loopback): + # Every request carries a management credential. Over http a network attacker reads it. + raise ConfigurationError( + f"Management client `base_url` must be https (got {parts.scheme}://{parts.hostname})." + ) + + path = parts.path.rstrip("/") + path = path if path.endswith("/api/v1") else f"{path}/api/v1" + + return f"{parts.scheme}://{parts.netloc}{path}" + + +def _origin(url: str) -> tuple[str, str, int | None]: + parts = urlsplit(url) + default = {"https": 443, "http": 80}.get(parts.scheme) + return (parts.scheme, (parts.hostname or "").lower(), parts.port or default) + + +def _query_pairs(query: Mapping[str, Any]) -> list[tuple[str, str]]: + pairs: list[tuple[str, str]] = [] + + def add(key: str, value: Any) -> None: + if value is None: + return + + if isinstance(value, (list, tuple)): + for item in value: + add(f"{key}[]", item) + return + + if isinstance(value, bool): + # Laravel's `boolean` rule takes 1/0, not the strings "true"/"false". + pairs.append((key, "1" if value else "0")) + return + + if isinstance(value, Mapping): + pairs.append((key, json.dumps(value, separators=(",", ":")))) + return + + pairs.append((key, str(value))) + + for key, value in query.items(): + add(key, value) + + return pairs + + +def _read_json(response: httpx.Response) -> Any: + if not response.content: + return None + + try: + return response.json() + except ValueError: + return None + + +@dataclass(frozen=True, slots=True) +class _Held: + approval: PendingApproval + retry_after: float | None + + +@dataclass(frozen=True, slots=True) +class _Request: + method: str + url: str + params: tuple[tuple[str, str], ...] | None + payload: Any + has_body: bool + idempotency_key: str | None + headers: Mapping[str, str] | None + + +class ManagementTransport: + """Runs operations for one plane: credentials, retries, approvals and paging. + + Every generated client holds one as ``client.core``. ``call()`` takes an + :class:`OperationSpec`, so a route the generator does not know can still be called with + one you build yourself — or through ``client.request(method, path, …)``. + """ + + def __init__( + self, + plane: Plane, + *, + base_url: str | None = None, + api_key: str | None = None, + access_token: AccessToken | None = None, + environment: str | None = None, + dpop: DPoPSigner | None = None, + on_approval_required: ApprovalHandler | None = None, + approval_poll_interval: float = 2.0, + retry: RetryOptions | None = None, + timeout: float = 30.0, + headers: Mapping[str, str] | None = None, + transport: httpx.BaseTransport | None = None, + http_client: httpx.Client | None = None, + ) -> None: + if base_url is None or base_url == "": + raise ConfigurationError( + f"The {plane} management client needs a `base_url` (the environment's own host)." + ) + + has_key = api_key is not None + has_token = access_token is not None + + if has_key == has_token: + raise ConfigurationError( + "Pass exactly one of `api_key` and `access_token` to a management client." + ) + + if api_key is not None: + expected = _KEY_PREFIX[plane] + + if api_key == "": + raise ConfigurationError("Management client `api_key` is empty.") + + if expected is None: + raise ConfigurationError( + f"The {plane} plane accepts no management key — only an access token a " + "person delegated (`access_token`)." + ) + + for prefix in _KEY_PREFIX.values(): + if prefix is not None and prefix != expected and api_key.startswith(prefix): + # Credentials never cross planes: the server would answer 401 to every request. + raise ConfigurationError( + f"A `{prefix}…` key cannot call the {plane} plane; " + f"it takes `{expected}…` keys." + ) + + if dpop is not None: + raise ConfigurationError( + "`dpop` applies to an `access_token`, not a management key." + ) + + if environment is not None: + if plane != "environment": + raise ConfigurationError( + f"`environment` applies to the environment plane, not the {plane} plane." + ) + + if environment == "": + raise ConfigurationError("Management client `environment` is empty.") + + if has_key: + raise ConfigurationError( + "`environment` names the environment for a root access token. A " + "`cbid_env_…` key is bound to its own environment's host: use that host " + "as `base_url` instead." + ) + + self.plane: Plane = plane + self.base_url = _normalize_base_url(base_url) + self._api_key = api_key + self._access_token = access_token + self._environment = environment + self._dpop = dpop + self._dpop_nonce: str | None = None + self._on_approval_required = on_approval_required + self._poll_interval = max(0.0, approval_poll_interval) + options = retry or RetryOptions() + self._max_retries = max(0, options.max_retries) + self._base_delay = max(0.0, options.base_delay) + self._max_delay = max(0.0, options.max_delay) + self._timeout = timeout + self._headers = dict(headers or {}) + self._owns_client = http_client is None + self._http = http_client or httpx.Client(timeout=timeout, transport=transport) + #: Sleeps between retries and polls. Replaceable, e.g. to cap waits in tests. + self.sleep: Callable[[float], None] = time.sleep + + def close(self) -> None: + """Close the HTTP client, when this transport created it.""" + if self._owns_client: + self._http.close() + + # ── Calls ──────────────────────────────────────────────────────────────────────────── + + @overload + def call( + self, + op: OperationSpec, + path_args: Sequence[str], + input: Any = None, + *, + approval: Literal["wait"] = "wait", + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any]: ... + + @overload + def call( + self, + op: OperationSpec, + path_args: Sequence[str], + input: Any = None, + *, + approval: ApprovalMode, + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any] | PendingApprovalResult[Any]: ... + + def call( + self, + op: OperationSpec, + path_args: Sequence[str], + input: Any = None, + *, + approval: ApprovalMode = "wait", + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any] | PendingApprovalResult[Any]: + """Run one operation. Generated methods call this; so can you, with an OperationSpec.""" + url = self._url(op, path_args) + params = None if op.body or not isinstance(input, Mapping) else _query_pairs(input) + key = (idempotency_key or str(uuid.uuid4())) if op.method in _WRITE_METHODS else None + request = _Request( + method=op.method, + url=url, + params=tuple(params) if params else None, + payload=input if op.body else None, + has_body=op.body and input is not None, + idempotency_key=key, + headers=headers, + ) + context = ApprovalContext( + action=op.action, danger=op.danger, method=op.method, path=op.path + ) + + response = self._send(request, approval_id) + held = self._held_approval(response) + + if held is not None and approval == "return": + return PendingApprovalResult( + approval=held.approval, + idempotency_key=key, + action=op.action, + _resume=lambda: self._approve_and_repeat(request, held, context, notify=False), + ) + + if held is not None: + return self._approve_and_repeat(request, held, context, notify=True) + + return self._result(response, key) + + def paginate( + self, + op: OperationSpec, + path_args: Sequence[str], + query: Mapping[str, Any] | None = None, + *, + headers: Mapping[str, str] | None = None, + ) -> Iterator[Any]: + """Every item of a paged list, fetching pages as the iteration reaches them. + + Works for both cursor (``after`` / ``meta.next_cursor``) and numbered (``page`` / + ``meta.next_page``) lists. + """ + current: dict[str, Any] = dict(query or {}) + + while True: + page = self.call(op, path_args, current, headers=headers) + items = page.data if isinstance(page.data, list) else [] + + yield from items + + meta = page.meta or {} + + if not items or meta.get("has_more") is not True: + return + + if op.pagination == "page": + raw = current.get("page", 1) + number = raw if isinstance(raw, int) else int(str(raw)) + next_page = meta.get("next_page") + current = {**current, "page": next_page if next_page is not None else number + 1} + else: + cursor = meta.get("next_cursor") + + if not isinstance(cursor, str) or cursor == "": + return + + current = {**current, "after": cursor} + + @overload + def request( + self, + method: HttpMethod, + path: str, + *, + query: Mapping[str, Any] | None = None, + body: Any = None, + approval: Literal["wait"] = "wait", + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any]: ... + + @overload + def request( + self, + method: HttpMethod, + path: str, + *, + query: Mapping[str, Any] | None = None, + body: Any = None, + approval: ApprovalMode, + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any] | PendingApprovalResult[Any]: ... + + def request( + self, + method: HttpMethod, + path: str, + *, + query: Mapping[str, Any] | None = None, + body: Any = None, + approval: ApprovalMode = "wait", + idempotency_key: str | None = None, + approval_id: str | None = None, + headers: Mapping[str, str] | None = None, + ) -> ApiResponse[Any] | PendingApprovalResult[Any]: + """Call a route the generated surface does not cover. ``path`` is relative to ``/api/v1``. + + Writes get an ``Idempotency-Key`` and the approval loop like any operation. + """ + op = OperationSpec( + action=None, + operation_id=None, + method=method, + path=path, + path_params=(), + scope=None, + danger=None, + approval=True, + body=body is not None, + pagination=None, + ) + + return self.call( + op, + (), + body if body is not None else query, + approval=approval, + idempotency_key=idempotency_key, + approval_id=approval_id, + headers=headers, + ) + + # ── Internals ──────────────────────────────────────────────────────────────────────── + + def _url(self, op: OperationSpec, path_args: Sequence[str]) -> str: + args = list(path_args) + path = op.path + start = 0 + + while (open_at := path.find("{", start)) != -1: + close_at = path.index("}", open_at) + name = path[open_at + 1 : close_at] + value = args.pop(0) if args else "" + + if not isinstance(value, str) or value == "": + raise TypeError(f"{op.action or op.path}: missing path parameter `{name}`.") + + encoded = quote(value, safe="") + path = f"{path[:open_at]}{encoded}{path[close_at + 1 :]}" + start = open_at + len(encoded) + + return f"{self.base_url}{path}" + + def _token(self) -> str: + source = self._access_token + token = source() if callable(source) else source + + if not isinstance(token, str) or token == "": + raise ConfigurationError( + "The management client `access_token` provider returned no token." + ) + + return token + + def _build(self, request: _Request, approval_id: str | None) -> httpx.Request: + headers = httpx.Headers({**self._headers, **(request.headers or {})}) + headers["Accept"] = "application/json" + + if request.has_body: + headers["Content-Type"] = "application/json" + + if request.idempotency_key is not None: + headers["Idempotency-Key"] = request.idempotency_key + + if approval_id is not None: + headers["Cbox-Approval"] = approval_id + + if self._environment is not None: + headers["Cbox-Environment"] = self._environment + + if self._api_key is not None: + headers["Authorization"] = f"Bearer {self._api_key}" + else: + token = self._token() + + if self._dpop is None: + headers["Authorization"] = f"Bearer {token}" + else: + headers["Authorization"] = f"DPoP {token}" + headers["DPoP"] = self._dpop.proof( + method=request.method, + url=request.url, + access_token=token, + nonce=self._dpop_nonce, + ) + + content = json.dumps(request.payload).encode() if request.has_body else None + + return self._http.build_request( + request.method, + request.url, + params=request.params, + headers=headers, + content=content, + timeout=self._timeout, + ) + + def _exchange(self, request: _Request, approval_id: str | None) -> httpx.Response: + """One HTTP exchange, plus the single DPoP-nonce challenge retry RFC 9449 §8 describes.""" + nonce_retry = 0 + + while True: + response = self._http.send(self._build(request, approval_id)) + nonce = response.headers.get("dpop-nonce") + + if nonce is not None and self._dpop is not None: + challenged = ( + response.status_code == 401 and nonce != self._dpop_nonce and nonce_retry == 0 + ) + self._dpop_nonce = nonce + + if challenged: + response.close() + nonce_retry += 1 + continue + + return response + + def _should_retry(self, response: httpx.Response) -> bool: + if response.status_code >= 500 or response.status_code == 429: + return True + + if response.status_code == 409: + # The first request with this Idempotency-Key is still running: its answer is coming. + body = _read_json(response) + return isinstance(body, dict) and body.get("error") == "idempotency_in_progress" + + return False + + def _backoff(self, attempt: int) -> float: + exponential: float = self._base_delay * 2**attempt + return min(self._max_delay, exponential / 2 + random.random() * (exponential / 2)) + + def _send(self, request: _Request, approval_id: str | None) -> httpx.Response: + """Send with retries. Every attempt carries the SAME Idempotency-Key: a write lands once.""" + attempt = 0 + + while True: + try: + response = self._exchange(request, approval_id) + except httpx.TransportError as exc: + if attempt >= self._max_retries: + path = urlsplit(request.url).path + raise ManagementNetworkError( + f"{request.method} {path} failed: {exc}", request.idempotency_key + ) from exc + + self.sleep(self._backoff(attempt)) + attempt += 1 + continue + + if attempt >= self._max_retries or not self._should_retry(response): + return response + + delay = retry_after_seconds(response.headers) + delay = self._backoff(attempt) if delay is None else delay + + if delay > self._max_delay: + return response + + response.close() + self.sleep(delay) + attempt += 1 + + def _held_approval(self, response: httpx.Response) -> _Held | None: + if response.status_code != 202: + return None + + body = _read_json(response) + + if not isinstance(body, dict) or body.get("error") != "approval_required": + return None + + approval = body.get("approval") + + if not isinstance(approval, dict): + return None + + approval_id = approval.get("id") + + if not isinstance(approval_id, str) or approval_id == "": + return None + + def text(name: str, default: str) -> str: + value = approval.get(name) + return value if isinstance(value, str) else default + + mount = _APPROVAL_MOUNT[self.plane] + fallback = f"{self.base_url}{mount}/action-approvals/{quote(approval_id, safe='')}" + + return _Held( + approval=PendingApproval( + id=approval_id, + status=text("status", "pending"), + binding_code=text("binding_code", ""), + expires_at=text("expires_at", ""), + poll_url=text("poll_url", fallback), + ), + retry_after=retry_after_seconds(response.headers), + ) + + def _approve_and_repeat( + self, request: _Request, first: _Held, context: ApprovalContext, *, notify: bool + ) -> ApiResponse[Any]: + held = first + round_ = 1 + + while True: + if (notify or round_ > 1) and self._on_approval_required is not None: + self._on_approval_required(held.approval, context) + + self._await_approval(held.approval, held.retry_after) + + response = self._send(request, held.approval.id) + again = self._held_approval(response) + + if again is None: + return self._result(response, request.idempotency_key) + + if round_ >= _MAX_APPROVAL_ROUNDS: + raise ApprovalError( + f"The request was held for approval {round_} times; giving up.", + "consumed", + again.approval, + ) + + held = again + round_ += 1 + + def _await_approval(self, approval: PendingApproval, initial_delay: float | None) -> None: + """Poll an approval until the person decides. Returns on ``approved``; raises otherwise.""" + poll_url = urljoin(self.base_url, approval.poll_url) + + if _origin(poll_url) != _origin(self.base_url): + # The poll carries the same credential as the request. Never hand it to another host. + scheme, host, _ = _origin(poll_url) + raise CboxIdError( + f"Refusing to poll approval {approval.id} at another origin ({scheme}://{host})." + ) + + expires_at = _parse_time(approval.expires_at) + delay = initial_delay if initial_delay is not None else self._poll_interval + poll = _Request("GET", poll_url, None, None, False, None, None) + + while True: + self.sleep(min(delay, 60.0)) + + response = self._send(poll, None) + + if not response.is_success: + raise self._error(response) + + body = _read_json(response) + data = body.get("data") if isinstance(body, dict) else None + status = data.get("status") if isinstance(data, dict) else None + + if status == "approved": + return + if status == "denied": + raise ApprovalDeniedError(approval) + if status == "expired": + raise ApprovalExpiredError(approval) + if status == "consumed": + raise ApprovalError( + f"Approval {approval.id} was already used by another request.", + "consumed", + approval, + ) + + if expires_at is not None and time.time() > expires_at + 30: + raise ApprovalExpiredError(approval) + + retry = retry_after_seconds(response.headers) + delay = retry if retry is not None else self._poll_interval + + def _result(self, response: httpx.Response, idempotency_key: str | None) -> ApiResponse[Any]: + if not response.is_success: + raise self._error(response) + + body = None if response.status_code == 204 else _read_json(response) + envelope = isinstance(body, dict) and "data" in body + meta = body.get("meta") if isinstance(body, dict) and envelope else None + + return ApiResponse( + status=response.status_code, + data=body["data"] if isinstance(body, dict) and envelope else body, + meta=meta if isinstance(meta, dict) else None, + body=body, + replayed=response.headers.get("idempotent-replayed", "").lower() == "true", + idempotency_key=idempotency_key, + request_id=response.headers.get("x-request-id"), + headers=response.headers, + ) + + def _error(self, response: httpx.Response) -> CboxIdApiError: + body = _read_json(response) + fields: dict[str, Any] = body if isinstance(body, dict) else {} + code = fields.get("error") + code = code if isinstance(code, str) else f"http_{response.status_code}" + # A bearer challenge (RFC 6750) says `error_description`, the management envelope `message`. + message = fields.get("message") + + if not isinstance(message, str): + description = fields.get("error_description") + message = ( + description if isinstance(description, str) else f"HTTP {response.status_code}" + ) + + errors: dict[str, list[str]] = {} + raw_errors = fields.get("errors") + + if isinstance(raw_errors, dict): + for name, messages in raw_errors.items(): + errors[str(name)] = ( + [str(m) for m in messages] if isinstance(messages, list) else [str(messages)] + ) + + retry_after = retry_after_seconds(response.headers) + # The envelope's `request_id` first: a proxy in between may rewrite or drop the header. + request_id = fields.get("request_id") + + if not isinstance(request_id, str) or request_id == "": + request_id = response.headers.get("x-request-id") + + return CboxIdApiError( + status=response.status_code, + error=code, + message=message, + errors=errors, + request_id=request_id, + retry_after=None if retry_after is None else math.ceil(retry_after), + ) + + +def _parse_time(value: str) -> float | None: + if value == "": + return None + + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + + return parsed.timestamp() diff --git a/tests/fixtures/management_surface.txt b/tests/fixtures/management_surface.txt new file mode 100644 index 0000000..8c27b88 --- /dev/null +++ b/tests/fixtures/management_surface.txt @@ -0,0 +1,282 @@ +environment.access_reviews.close(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview] +environment.access_reviews.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview] +environment.access_reviews.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[AccessReview] | PendingApprovalResult[AccessReview] +environment.access_reviews.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[AccessReviewsListData] | PendingApprovalResult[AccessReviewsListData] +environment.access_reviews.list_all(query=, *, headers=) -> Iterator[AccessReview] +environment.access_reviews.items.decide(id, item_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AccessReviewItem] | PendingApprovalResult[AccessReviewItem] +environment.access_reviews.items.list(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[AccessReviewsItemsListData] | PendingApprovalResult[AccessReviewsItemsListData] +environment.access_reviews.items.list_all(id, query=, *, headers=) -> Iterator[AccessReviewItem] +environment.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovalsGetData] +environment.api_keys.list(organization_id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[ApiKeysListData] | PendingApprovalResult[ApiKeysListData] +environment.api_keys.list_all(organization_id, query=, *, headers=) -> Iterator[ApiKey] +environment.api_keys.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.apis.create(body, *, idempotency_key=, headers=) -> ApiResponse[Api] +environment.apis.delete(id, *, idempotency_key=, headers=) -> ApiResponse[None] +environment.apis.get(id, *, headers=) -> ApiResponse[Api] +environment.apis.list(query=, *, headers=) -> ApiResponse[ApisListData] +environment.apis.list_all(query=, *, headers=) -> Iterator[Api] +environment.apis.update(id, body=, *, idempotency_key=, headers=) -> ApiResponse[Api] +environment.apis.scopes.define(id, key, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Api] | PendingApprovalResult[Api] +environment.apis.scopes.remove(id, key, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.approvals.deny(request_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.approvals.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[ApprovalsListData] | PendingApprovalResult[ApprovalsListData] +environment.approvals.list_all(query=, *, headers=) -> Iterator[AgentRequest] +environment.apps.blueprint(id, *, headers=) -> ApiResponse[AppBlueprint] +environment.apps.copy(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.create(body=, *, idempotency_key=, headers=) -> ApiResponse[App] +environment.apps.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.apps.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.list(query=, *, headers=) -> ApiResponse[AppsListData] +environment.apps.list_all(query=, *, headers=) -> Iterator[App] +environment.apps.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.manifest.set(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.manifest.sync(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AppsManifestSyncData] | PendingApprovalResult[AppsManifestSyncData] +environment.apps.scopes.set(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.secrets.list(id, *, headers=) -> ApiResponse[AppsSecretsListData] +environment.apps.secrets.revoke(id, secret_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.apps.secrets.rotate(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AppSecret] | PendingApprovalResult[AppSecret] +environment.apps.settings.api_key_prefix(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.settings.backchannel_logout(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.settings.token_exchange(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.apps.settings.token_lifetime(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] +environment.audit.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[AuditListData] | PendingApprovalResult[AuditListData] +environment.audit.list_all(query=, *, headers=) -> Iterator[AuditEntry] +environment.audit_logs.verify(query=, *, approval=, approval_id=, headers=) -> ApiResponse[AuditLogVerification] | PendingApprovalResult[AuditLogVerification] +environment.audit_logs.events.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AuditLogEventBatch] | PendingApprovalResult[AuditLogEventBatch] +environment.audit_logs.events.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[AuditLogsEventsListData] | PendingApprovalResult[AuditLogsEventsListData] +environment.audit_logs.events.list_all(query=, *, headers=) -> Iterator[AuditLogEvent] +environment.audit_logs.exports.create(body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AuditLogExport] | PendingApprovalResult[AuditLogExport] +environment.audit_logs.exports.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[AuditLogExport] | PendingApprovalResult[AuditLogExport] +environment.audit_logs.schemas.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema] +environment.audit_logs.schemas.delete(action, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.audit_logs.schemas.get(action, *, approval=, approval_id=, headers=) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema] +environment.audit_logs.schemas.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[AuditLogsSchemasListData] | PendingApprovalResult[AuditLogsSchemasListData] +environment.audit_logs.schemas.list_all(query=, *, headers=) -> Iterator[AuditLogSchema] +environment.audit_logs.schemas.update(action, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AuditLogSchema] | PendingApprovalResult[AuditLogSchema] +environment.audit_logs.settings.get(*, approval=, approval_id=, headers=) -> ApiResponse[AuditLogSettings] | PendingApprovalResult[AuditLogSettings] +environment.audit_logs.settings.update(body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AuditLogSettings] | PendingApprovalResult[AuditLogSettings] +environment.branding.appearance.get(query=, *, approval=, approval_id=, headers=) -> ApiResponse[Appearance] | PendingApprovalResult[Appearance] +environment.branding.appearance.set(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Appearance] | PendingApprovalResult[Appearance] +environment.branding.whitelabel.get(query=, *, approval=, approval_id=, headers=) -> ApiResponse[WhitelabelBranding] | PendingApprovalResult[WhitelabelBranding] +environment.branding.whitelabel.set(body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[WhitelabelBranding] | PendingApprovalResult[WhitelabelBranding] +environment.directories.connect(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Directory] | PendingApprovalResult[Directory] +environment.directories.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Directory] | PendingApprovalResult[Directory] +environment.directories.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.directories.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[Directory] | PendingApprovalResult[Directory] +environment.directories.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[DirectoriesListData] | PendingApprovalResult[DirectoriesListData] +environment.directories.list_all(query=, *, headers=) -> Iterator[Directory] +environment.directories.update(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Directory] | PendingApprovalResult[Directory] +environment.directories.groups.list(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[DirectoriesGroupsListData] | PendingApprovalResult[DirectoriesGroupsListData] +environment.directories.groups.list_all(id, query=, *, headers=) -> Iterator[DirectoryGroup] +environment.directories.groups.map(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[DirectoryGroup] | PendingApprovalResult[DirectoryGroup] +environment.directories.status.set(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Directory] | PendingApprovalResult[Directory] +environment.directories.token.rotate(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Directory] | PendingApprovalResult[Directory] +environment.domains.add(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain] +environment.domains.get(*, approval=, approval_id=, headers=) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain] +environment.domains.remove(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.domains.verify(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain] +environment.events.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[EventsListData] | PendingApprovalResult[EventsListData] +environment.events.list_all(query=, *, headers=) -> Iterator[DomainEvent] +environment.frontend_keys.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[FrontendKey] | PendingApprovalResult[FrontendKey] +environment.frontend_keys.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[FrontendKeysListData] | PendingApprovalResult[FrontendKeysListData] +environment.frontend_keys.list_all(query=, *, headers=) -> Iterator[FrontendKey] +environment.frontend_keys.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.frontend_keys.set_origins(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[FrontendKey] | PendingApprovalResult[FrontendKey] +environment.hooks.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook] +environment.hooks.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.hooks.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook] +environment.hooks.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[HooksListData] | PendingApprovalResult[HooksListData] +environment.hooks.list_all(query=, *, headers=) -> Iterator[InlineHook] +environment.hooks.update(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[InlineHook] | PendingApprovalResult[InlineHook] +environment.invitations.list(organization_id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[InvitationsListData] | PendingApprovalResult[InvitationsListData] +environment.invitations.list_all(organization_id, query=, *, headers=) -> Iterator[Invitation] +environment.invitations.resend(organization_id, invitation_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation] +environment.invitations.revoke(organization_id, invitation_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.invitations.send(organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation] +environment.keys.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[KeysCreateData] | PendingApprovalResult[KeysCreateData] +environment.keys.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[KeysListData] | PendingApprovalResult[KeysListData] +environment.keys.list_all(query=, *, headers=) -> Iterator[KeysListItem] +environment.keys.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.keys.rotate(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[KeysRotateData] | PendingApprovalResult[KeysRotateData] +environment.legacy_login.approve(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin] +environment.legacy_login.get(*, approval=, approval_id=, headers=) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin] +environment.legacy_login.probe(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLoginProbeData] | PendingApprovalResult[LegacyLoginProbeData] +environment.legacy_login.revoke(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin] +environment.log_streams.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream] +environment.log_streams.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.log_streams.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream] +environment.log_streams.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[LogStreamsListData] | PendingApprovalResult[LogStreamsListData] +environment.log_streams.list_all(query=, *, headers=) -> Iterator[LogStream] +environment.log_streams.test(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStreamTest] | PendingApprovalResult[LogStreamTest] +environment.log_streams.update(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream] +environment.members.add(organization_id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +environment.members.list(organization_id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[MembersListData] | PendingApprovalResult[MembersListData] +environment.members.list_all(organization_id, query=, *, headers=) -> Iterator[Member] +environment.members.remove(organization_id, user_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.members.update(organization_id, user_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +environment.members.roles.grant(organization_id, user_id, role_id, query=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment] +environment.members.roles.list(organization_id, user_id, *, approval=, approval_id=, headers=) -> ApiResponse[MembersRolesListData] | PendingApprovalResult[MembersRolesListData] +environment.members.roles.revoke(organization_id, user_id, role_id, query=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.organizations.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +environment.organizations.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +environment.organizations.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +environment.organizations.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[OrganizationsListData] | PendingApprovalResult[OrganizationsListData] +environment.organizations.list_all(query=, *, headers=) -> Iterator[Organization] +environment.organizations.reactivate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +environment.organizations.suspend(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +environment.organizations.transfer_ownership(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +environment.organizations.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +environment.organizations.domains.add(organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain] +environment.organizations.domains.capture(organization_id, domain_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain] +environment.organizations.domains.list(organization_id, *, approval=, approval_id=, headers=) -> ApiResponse[OrganizationsDomainsListData] | PendingApprovalResult[OrganizationsDomainsListData] +environment.organizations.domains.remove(organization_id, domain_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.organizations.domains.verify(organization_id, domain_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain] +environment.organizations.portal_links.create(organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PortalLink] | PendingApprovalResult[PortalLink] +environment.permissions.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Permission] | PendingApprovalResult[Permission] +environment.permissions.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.permissions.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[PermissionsListData] | PendingApprovalResult[PermissionsListData] +environment.permissions.list_all(query=, *, headers=) -> Iterator[Permission] +environment.permissions.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Permission] | PendingApprovalResult[Permission] +environment.provisioning.targets.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget] +environment.provisioning.targets.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.provisioning.targets.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget] +environment.provisioning.targets.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[ProvisioningTargetsListData] | PendingApprovalResult[ProvisioningTargetsListData] +environment.provisioning.targets.list_all(query=, *, headers=) -> Iterator[ProvisioningTarget] +environment.provisioning.targets.status.set(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ProvisioningTarget] | PendingApprovalResult[ProvisioningTarget] +environment.roles.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Role] | PendingApprovalResult[Role] +environment.roles.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.roles.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[Role] | PendingApprovalResult[Role] +environment.roles.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[RolesListData] | PendingApprovalResult[RolesListData] +environment.roles.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Role] | PendingApprovalResult[Role] +environment.roles.permissions.grant(id, permission_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Role] | PendingApprovalResult[Role] +environment.roles.permissions.revoke(id, permission_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Role] | PendingApprovalResult[Role] +environment.saml_apps.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp] +environment.saml_apps.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.saml_apps.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp] +environment.saml_apps.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SamlAppsListData] | PendingApprovalResult[SamlAppsListData] +environment.saml_apps.list_all(query=, *, headers=) -> Iterator[SamlApp] +environment.saml_apps.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SamlApp] | PendingApprovalResult[SamlApp] +environment.signin.policy.get(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy] +environment.signin.policy.inherit(organization_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.signin.policy.update(body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy] +environment.signin.self_service_signup.set(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SigninSelfServiceSignupSetData] | PendingApprovalResult[SigninSelfServiceSignupSetData] +environment.signin.social.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.signin.social.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SigninSocialListData] | PendingApprovalResult[SigninSocialListData] +environment.signin.social.list_all(query=, *, headers=) -> Iterator[SocialProvider] +environment.signin.social.set(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SocialProvider] | PendingApprovalResult[SocialProvider] +environment.sod_policies.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy] +environment.sod_policies.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.sod_policies.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy] +environment.sod_policies.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SodPoliciesListData] | PendingApprovalResult[SodPoliciesListData] +environment.sod_policies.list_all(query=, *, headers=) -> Iterator[SodPolicy] +environment.sod_policies.status.set(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SodPolicy] | PendingApprovalResult[SodPolicy] +environment.sso.connections.activate(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection] +environment.sso.connections.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection] +environment.sso.connections.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.sso.connections.disable(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection] +environment.sso.connections.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection] +environment.sso.connections.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SsoConnectionsListData] | PendingApprovalResult[SsoConnectionsListData] +environment.sso.connections.list_all(query=, *, headers=) -> Iterator[SsoConnection] +environment.sso.connections.require_sso(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy] +environment.sso.connections.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoConnection] | PendingApprovalResult[SsoConnection] +environment.sso.connections.certificates.activate(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates] +environment.sso.connections.certificates.list(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates] +environment.sso.connections.certificates.stage(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoCertificates] | PendingApprovalResult[SsoCertificates] +environment.sso.domains.capture(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain] +environment.sso.domains.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain] +environment.sso.domains.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.sso.domains.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SsoDomainsListData] | PendingApprovalResult[SsoDomainsListData] +environment.sso.domains.list_all(query=, *, headers=) -> Iterator[SsoDomain] +environment.sso.domains.verify(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SsoDomain] | PendingApprovalResult[SsoDomain] +environment.sso.saml_metadata.import_(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SamlMetadata] | PendingApprovalResult[SamlMetadata] +environment.support_sessions.end(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.support_sessions.start(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SupportSession] | PendingApprovalResult[SupportSession] +environment.token_vault.grants.create(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret] +environment.token_vault.grants.delete(id, client_id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.token_vault.secrets.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret] +environment.token_vault.secrets.get(id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret] +environment.token_vault.secrets.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[TokenVaultSecretsListData] | PendingApprovalResult[TokenVaultSecretsListData] +environment.token_vault.secrets.list_all(query=, *, headers=) -> Iterator[TokenVaultSecret] +environment.token_vault.secrets.revoke(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret] +environment.token_vault.secrets.rotate(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[TokenVaultSecret] | PendingApprovalResult[TokenVaultSecret] +environment.users.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.deactivate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.erase(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ErasureReceipt] | PendingApprovalResult[ErasureReceipt] +environment.users.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[UsersListData] | PendingApprovalResult[UsersListData] +environment.users.list_all(query=, *, headers=) -> Iterator[User] +environment.users.reactivate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.verify(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.environment_roles.get(id, role_id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment] +environment.users.environment_roles.grant(id, role_id, query=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[RoleAssignment] | PendingApprovalResult[RoleAssignment] +environment.users.environment_roles.list(id, *, approval=, approval_id=, headers=) -> ApiResponse[UsersEnvironmentRolesListData] | PendingApprovalResult[UsersEnvironmentRolesListData] +environment.users.environment_roles.revoke(id, role_id, query=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.users.mfa.reset(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.users.password.set(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[User] | PendingApprovalResult[User] +environment.users.password_reset.send(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.users.sessions.list(id, *, approval=, approval_id=, headers=) -> ApiResponse[UsersSessionsListData] | PendingApprovalResult[UsersSessionsListData] +environment.users.sessions.revoke(id, session_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.users.sessions.revoke_all(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.users.verification.send(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.webhooks.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +environment.webhooks.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.webhooks.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +environment.webhooks.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[WebhooksListData] | PendingApprovalResult[WebhooksListData] +environment.webhooks.list_all(query=, *, headers=) -> Iterator[Webhook] +environment.webhooks.pause(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +environment.webhooks.resume(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +environment.webhooks.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +environment.webhooks.secret.rotate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +workspace.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovalsGetData] +workspace.environments.create(body, *, idempotency_key=, headers=) -> ApiResponse[CreatedEnvironment] +workspace.environments.list(query=, *, headers=) -> ApiResponse[EnvironmentsListData] +workspace.environments.list_all(query=, *, headers=) -> Iterator[Environment] +workspace.environments.domain.remove(environment_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +workspace.environments.domain.request(environment_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain] +workspace.environments.domain.verify(environment_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain] +workspace.keys.environment.create(environment_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[EnvironmentKey] | PendingApprovalResult[EnvironmentKey] +workspace.keys.environment.revoke(environment_id, id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +workspace.keys.workspace.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[WorkspaceKey] | PendingApprovalResult[WorkspaceKey] +workspace.keys.workspace.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[KeysWorkspaceListData] | PendingApprovalResult[KeysWorkspaceListData] +workspace.keys.workspace.list_all(query=, *, headers=) -> Iterator[WorkspaceKey] +workspace.keys.workspace.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +workspace.projects.create(body, *, idempotency_key=, headers=) -> ApiResponse[Project] +workspace.projects.list(*, headers=) -> ApiResponse[ProjectsListData] +workspace.projects.reactivate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] +workspace.projects.rename(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] +workspace.projects.suspend(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] +workspace.projects.verification.resend(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ProjectsVerificationResendData] | PendingApprovalResult[ProjectsVerificationResendData] +workspace.team.environment_access(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +workspace.team.invite(body, *, idempotency_key=, headers=) -> ApiResponse[Member] +workspace.team.list(query=, *, headers=) -> ApiResponse[TeamListData] +workspace.team.list_all(query=, *, headers=) -> Iterator[Member] +workspace.team.remove(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +workspace.team.role(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +workspace.team.transfer_ownership(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +workspace.team.invitations.list(*, headers=) -> ApiResponse[TeamInvitationsListData] +workspace.team.invitations.resend(id, *, idempotency_key=, headers=) -> ApiResponse[Member] +workspace.team.invitations.revoke(id, *, idempotency_key=, headers=) -> ApiResponse[None] +workspace.workspace.get(*, headers=) -> ApiResponse[Organization] +workspace.workspace.settings.update(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] +platform.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovalsGetData] +platform.environments.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformEnvironment] | PendingApprovalResult[PlatformEnvironment] +platform.environments.provision(environment_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ProvisionedEnvironment] | PendingApprovalResult[ProvisionedEnvironment] +platform.operators.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformOperator] | PendingApprovalResult[PlatformOperator] +platform.operators.set_status(operator_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformOperator] | PendingApprovalResult[PlatformOperator] +platform.organizations.create(environment_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization] +platform.organizations.move(environment_id, organization_id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization] +platform.organizations.set_status(environment_id, organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization] +platform.workspaces.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ProvisionedWorkspace] | PendingApprovalResult[ProvisionedWorkspace] +platform.workspaces.set_status(workspace_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformOrganization] | PendingApprovalResult[PlatformOrganization] +account.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovalsGetData] +account.api_keys.create(organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PersonalApiKey] | PendingApprovalResult[PersonalApiKey] +account.api_keys.revoke(organization_id, key_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +account.applications.revoke(client_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +account.devices.remove(device_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +account.organizations.leave(organization_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +account.passkeys.remove(passkey_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +account.profile.update(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Profile] | PendingApprovalResult[Profile] +account.sessions.revoke(session_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +account.sessions.revoke_others(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SessionsRevoked] | PendingApprovalResult[SessionsRevoked] +account.social.unlink(provider, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] diff --git a/tests/management_helpers.py b/tests/management_helpers.py new file mode 100644 index 0000000..3ba84fe --- /dev/null +++ b/tests/management_helpers.py @@ -0,0 +1,95 @@ +"""A fake management API: answers from a queue and records every request it saw.""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +import httpx + +from cbox_id.management import EnvironmentClient, RetryOptions + +HOST = "https://acme.test" +API = f"{HOST}/api/v1" +KEY = "cbid_env_test_key" + + +@dataclass +class Call: + method: str + url: str + headers: httpx.Headers + body: Any + + +Reply = httpx.Response | Exception | Callable[[Call], httpx.Response | Exception] + + +class FakeApi: + def __init__(self, *replies: Reply) -> None: + self.replies: list[Reply] = list(replies) + self.calls: list[Call] = [] + self.transport = httpx.MockTransport(self._handle) + + def _handle(self, request: httpx.Request) -> httpx.Response: + content = request.read() + call = Call( + method=request.method, + url=str(request.url), + headers=request.headers, + body=json.loads(content) if content else None, + ) + self.calls.append(call) + + if not self.replies: + raise AssertionError(f"unexpected request {call.method} {call.url}") + + reply = self.replies.pop(0) + answer = reply(call) if callable(reply) else reply + + if isinstance(answer, Exception): + raise answer + + return answer + + +def json_response( + body: object, status: int = 200, headers: dict[str, str] | None = None +) -> httpx.Response: + return httpx.Response(status, json=body, headers=headers or {}) + + +def env_client(fake: FakeApi, **options: Any) -> EnvironmentClient: + settings: dict[str, Any] = { + "base_url": HOST, + "api_key": KEY, + "transport": fake.transport, + "retry": RetryOptions(base_delay=0), + "approval_poll_interval": 0, + **options, + } + return EnvironmentClient(**settings) + + +def held(apr: str = "apr_1", poll_url: str | None = None) -> httpx.Response: + return json_response( + { + "error": "approval_required", + "message": "This action needs approval.", + "approval": { + "id": apr, + "status": "pending", + "binding_code": "K7-4Q", + "expires_at": "2999-01-01T00:00:00Z", + "poll_url": poll_url or f"{API}/action-approvals/{apr}", + }, + }, + 202, + {"Retry-After": "0"}, + ) + + +def approval_status(status: str, apr: str = "apr_1") -> httpx.Response: + return json_response({"data": {"id": apr, "status": status}}, 200, {"Retry-After": "0"}) diff --git a/tests/test_management.py b/tests/test_management.py new file mode 100644 index 0000000..47851b7 --- /dev/null +++ b/tests/test_management.py @@ -0,0 +1,714 @@ +"""The management clients: requests, idempotency, approvals, errors, paging, credentials.""" + +from __future__ import annotations + +import base64 +import hashlib +import inspect +import os +import re +from pathlib import Path +from typing import Any + +import httpx +import jwt +import pytest +from typing_extensions import assert_type + +from cbox_id.management import ( + ENVIRONMENT_OPERATIONS, + WORKSPACE_OPERATIONS, + AccountClient, + ApiResponse, + ApprovalContext, + ApprovalDeniedError, + ApprovalExpiredError, + CboxIdApiError, + CboxIdError, + ConfigurationError, + EnvironmentClient, + ES256DPoPSigner, + ManagementClient, + ManagementNetworkError, + PendingApproval, + PendingApprovalResult, + PlatformClient, + RetryOptions, + WorkspaceClient, + generate_dpop_key, +) +from cbox_id.management.generated.environment import App, AppSecret, Organization +from scripts.generate_management import generate_all + +from .management_helpers import ( + API, + HOST, + KEY, + FakeApi, + approval_status, + env_client, + held, + json_response, +) + +APP = {"id": "app_1", "client_id": "cid", "name": "Billing"} +UUID4 = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + + +# ── Requests ───────────────────────────────────────────────────────────────────────────── + + +def test_sends_the_key_as_a_bearer_token_and_unwraps_the_envelope() -> None: + fake = FakeApi(json_response({"data": APP}, 201)) + result = env_client(fake).apps.create({"name": "Billing", "type": "web"}) + + call = fake.calls[0] + assert call.url == f"{API}/apps" + assert call.method == "POST" + assert call.headers["authorization"] == f"Bearer {KEY}" + assert call.headers["content-type"] == "application/json" + assert call.body == {"name": "Billing", "type": "web"} + assert result.status == 201 + assert dict(result.data) == APP + assert_type(result, ApiResponse[App]) + assert result.replayed is False + assert result.pending is False + + +def test_path_parameters_are_encoded_in_order_and_get_input_goes_in_the_query() -> None: + fake = FakeApi( + json_response({"data": [], "meta": {"has_more": False}}), + json_response({"data": []}), + ) + client = env_client(fake) + + client.members.list("org/1", {"limit": 5}) + client.roles.list({"client_id": "cid", "organization_id": "org_1"}) + + assert fake.calls[0].url == f"{API}/organizations/org%2F1/members?limit=5" + assert "idempotency-key" not in fake.calls[0].headers + assert fake.calls[1].url == f"{API}/roles?client_id=cid&organization_id=org_1" + + +def test_query_lists_and_booleans_take_the_shape_laravel_reads() -> None: + fake = FakeApi(json_response({"data": []})) + env_client(fake).request( + "GET", "/things", query={"ids": ["a", "b"], "active": True, "gone": False, "skip": None} + ) + + assert fake.calls[0].url == f"{API}/things?ids%5B%5D=a&ids%5B%5D=b&active=1&gone=0" + + +def test_a_204_has_no_data() -> None: + fake = FakeApi(httpx.Response(204)) + result = env_client(fake).apps.delete("app_1") + + assert result.status == 204 + assert result.data is None + + +# ── Idempotency and retries ────────────────────────────────────────────────────────────── + + +def test_a_write_reuses_one_generated_idempotency_key_on_every_retry() -> None: + fake = FakeApi( + json_response({"error": "server_error", "message": "Boom."}, 503), + httpx.ConnectError("connection refused"), + json_response( + {"error": "rate_limited", "message": "Slow down."}, 429, {"Retry-After": "0"} + ), + json_response({"data": APP}, 201, {"Idempotent-Replayed": "true"}), + ) + result = env_client(fake).apps.create({"name": "Billing"}) + + assert len(fake.calls) == 4 + keys = {call.headers["idempotency-key"] for call in fake.calls} + assert len(keys) == 1 + (key,) = keys + assert re.match(UUID4, key) + assert all(call.body == {"name": "Billing"} for call in fake.calls) + assert result.replayed is True + assert result.idempotency_key == key + + +def test_uses_the_callers_key_and_a_fresh_one_per_call_otherwise() -> None: + fake = FakeApi(*(json_response({"data": APP}, 201) for _ in range(3))) + client = env_client(fake) + + client.apps.create({"name": "A"}, idempotency_key="mine-1") + client.apps.create({"name": "A"}) + client.apps.create({"name": "A"}) + + assert fake.calls[0].headers["idempotency-key"] == "mine-1" + assert fake.calls[1].headers["idempotency-key"] != fake.calls[2].headers["idempotency-key"] + + +def test_waits_out_409_idempotency_in_progress_with_the_same_key() -> None: + fake = FakeApi( + json_response( + {"error": "idempotency_in_progress", "message": "Still running."}, + 409, + {"Retry-After": "0"}, + ), + json_response({"data": APP}, 201, {"Idempotent-Replayed": "true"}), + ) + result = env_client(fake).apps.create({"name": "A"}) + + assert fake.calls[0].headers["idempotency-key"] == fake.calls[1].headers["idempotency-key"] + assert result.replayed is True + + +def test_honours_retry_after_and_never_waits_longer_than_max_delay() -> None: + fake = FakeApi( + json_response({"error": "rate_limited", "message": "Slow."}, 429, {"Retry-After": "7"}), + json_response({"data": []}), + ) + client = env_client(fake, retry=RetryOptions(base_delay=0, max_delay=10)) + slept: list[float] = [] + client.core.sleep = slept.append + + client.apps.list() + + assert slept == [7.0] + + +def test_does_not_retry_a_4xx_and_gives_up_after_max_retries() -> None: + conflict = FakeApi(json_response({"error": "slug_taken", "message": "Taken."}, 409)) + + with pytest.raises(CboxIdApiError) as caught: + env_client(conflict).organizations.create({"name": "Acme", "slug": "acme"}) + + assert caught.value.status == 409 + assert caught.value.error == "slug_taken" + assert len(conflict.calls) == 1 + + down = FakeApi(httpx.ConnectError("a"), httpx.ConnectError("b"), httpx.ConnectError("c")) + + with pytest.raises(ManagementNetworkError) as network: + env_client(down, retry=RetryOptions(max_retries=2, base_delay=0)).apps.create( + {"name": "A"}, idempotency_key="k-1" + ) + + assert network.value.idempotency_key == "k-1" + assert isinstance(network.value.__cause__, httpx.ConnectError) + assert len(down.calls) == 3 + + +def test_raises_a_429_at_once_when_retry_after_is_longer_than_it_may_wait() -> None: + fake = FakeApi( + json_response( + {"error": "rate_limited", "message": "Slow down."}, 429, {"Retry-After": "120"} + ) + ) + + with pytest.raises(CboxIdApiError) as caught: + env_client(fake).apps.list() + + assert caught.value.status == 429 + assert caught.value.retry_after == 120 + assert len(fake.calls) == 1 + + +# ── Errors ─────────────────────────────────────────────────────────────────────────────── + + +def test_types_a_validation_failure_and_never_echoes_the_request_body() -> None: + fake = FakeApi( + json_response( + { + "error": "validation_failed", + "message": "The given data was invalid.", + "errors": {"name": ["The name field is required."]}, + "request_id": "req_42", + }, + 422, + {"X-Request-Id": "req_from_header"}, + ) + ) + + with pytest.raises(CboxIdApiError) as caught: + env_client(fake).users.create({"email": "a@b.test", "password": "hunter2-secret"}) + + error = caught.value + assert error.status == 422 + assert error.error == "validation_failed" + assert error.is_validation_error is True + assert error.message == "The given data was invalid." + assert str(error) == "The given data was invalid." + assert error.errors == {"name": ["The name field is required."]} + assert error.request_id == "req_42" + assert "hunter2" not in repr(error) and "hunter2" not in str(error) + assert isinstance(error, CboxIdError) + + +def test_falls_back_to_the_x_request_id_header() -> None: + fake = FakeApi( + httpx.Response(404, text="Not found", headers={"X-Request-Id": "req_7"}) + ) + + with pytest.raises(CboxIdApiError) as caught: + env_client(fake).apps.get("app_1") + + assert caught.value.status == 404 + assert caught.value.request_id == "req_7" + + +def test_reads_a_bearer_challenge_and_a_non_json_body() -> None: + challenge = FakeApi( + json_response({"error": "invalid_token", "error_description": "Expired."}, 401) + ) + + with pytest.raises(CboxIdApiError) as caught: + env_client(challenge).apps.list() + + assert (caught.value.error, caught.value.message, caught.value.status) == ( + "invalid_token", + "Expired.", + 401, + ) + + html = FakeApi(httpx.Response(403, text="Forbidden")) + + with pytest.raises(CboxIdApiError) as forbidden: + env_client(html).apps.list() + + assert (forbidden.value.error, forbidden.value.message) == ("http_403", "HTTP 403") + + +# ── Approvals ──────────────────────────────────────────────────────────────────────────── + + +def test_shows_the_binding_code_polls_then_repeats_with_cbox_approval_and_the_same_key() -> None: + secret = {"id": "sec_2", "secret": "cbid_secret_shown_once", "ends_with": "once"} + fake = FakeApi( + held(), + approval_status("pending"), + approval_status("approved"), + json_response({"data": secret}, 201), + ) + seen: list[tuple[PendingApproval, ApprovalContext]] = [] + client = env_client(fake, on_approval_required=lambda a, c: seen.append((a, c))) + + result = client.apps.secrets.rotate("app_1", {"grace_seconds": 3600}) + + assert len(seen) == 1 + approval, context = seen[0] + assert (approval.id, approval.binding_code) == ("apr_1", "K7-4Q") + assert (context.action, context.danger) == ("apps.secrets.rotate", "critical") + assert [f"{c.method} {c.url}" for c in fake.calls] == [ + f"POST {API}/apps/app_1/secrets", + f"GET {API}/action-approvals/apr_1", + f"GET {API}/action-approvals/apr_1", + f"POST {API}/apps/app_1/secrets", + ] + assert "cbox-approval" not in fake.calls[0].headers + assert fake.calls[1].headers["authorization"] == f"Bearer {KEY}" + assert fake.calls[3].headers["cbox-approval"] == "apr_1" + assert fake.calls[3].headers["idempotency-key"] == fake.calls[0].headers["idempotency-key"] + assert fake.calls[3].body == {"grace_seconds": 3600} + assert_type(result, ApiResponse[AppSecret]) + assert result.data == secret + + +def test_raises_a_typed_error_when_the_person_denies_it_or_nobody_answers() -> None: + denied = FakeApi(held(), approval_status("denied")) + + with pytest.raises(ApprovalDeniedError): + env_client(denied).apps.secrets.rotate("app_1", {"grace_seconds": 0}) + + assert len(denied.calls) == 2 + + expired = FakeApi(held(), approval_status("expired")) + + with pytest.raises(ApprovalExpiredError) as caught: + env_client(expired).apps.secrets.rotate("app_1", {"grace_seconds": 0}) + + assert caught.value.approval.id == "apr_1" + assert caught.value.reason == "expired" + + +def test_return_mode_hands_back_the_pending_approval_and_resume_finishes() -> None: + fake = FakeApi( + held(), approval_status("approved"), json_response({"data": {"id": "sec_3"}}, 201) + ) + seen: list[PendingApproval] = [] + client = env_client(fake, on_approval_required=lambda a, _: seen.append(a)) + + outcome = client.apps.secrets.rotate("app_1", {"grace_seconds": 0}, approval="return") + + assert_type(outcome, ApiResponse[AppSecret] | PendingApprovalResult[AppSecret]) + assert isinstance(outcome, PendingApprovalResult) + assert outcome.pending is True + assert outcome.status == 202 + assert outcome.approval.binding_code == "K7-4Q" + assert outcome.action == "apps.secrets.rotate" + assert len(fake.calls) == 1 + assert seen == [] + + done = outcome.resume() + + assert_type(done, ApiResponse[AppSecret]) + assert done.data == {"id": "sec_3"} + assert fake.calls[2].headers["cbox-approval"] == "apr_1" + assert fake.calls[2].headers["idempotency-key"] == outcome.idempotency_key + + +def test_never_sends_the_credential_to_a_poll_url_on_another_origin() -> None: + fake = FakeApi(held("apr_1", "https://evil.test/api/v1/action-approvals/apr_1")) + + with pytest.raises(CboxIdError, match="another origin"): + env_client(fake).apps.secrets.rotate("app_1", {"grace_seconds": 0}) + + assert len(fake.calls) == 1 + + +def test_polls_the_workspace_planes_own_approval_route() -> None: + fake = FakeApi( + held("apr_9", "https://api.cboxid.test/api/v1/workspace/action-approvals/apr_9"), + approval_status("approved", "apr_9"), + httpx.Response(204), + ) + ws = WorkspaceClient( + base_url="https://api.cboxid.test", + api_key="cbid_ws_k", + transport=fake.transport, + retry=RetryOptions(base_delay=0), + approval_poll_interval=0, + ) + ws.keys.workspace.revoke("wk_1") + + assert fake.calls[1].url == "https://api.cboxid.test/api/v1/workspace/action-approvals/apr_9" + assert fake.calls[2].headers["cbox-approval"] == "apr_9" + + +def test_a_relative_poll_url_and_a_missing_one_resolve_on_the_planes_own_host() -> None: + fake = FakeApi( + held("apr_2", "/api/v1/action-approvals/apr_2"), + approval_status("approved", "apr_2"), + json_response({"data": {"id": "sec"}}, 201), + ) + env_client(fake).apps.secrets.rotate("app_1", {"grace_seconds": 0}) + + assert fake.calls[1].url == f"{API}/action-approvals/apr_2" + + +# ── Pagination ─────────────────────────────────────────────────────────────────────────── + + +def test_follows_next_cursor_on_a_cursor_paged_list() -> None: + fake = FakeApi( + json_response( + { + "data": [{"id": "o1"}, {"id": "o2"}], + "meta": {"limit": 2, "has_more": True, "next_cursor": "c2"}, + } + ), + json_response( + {"data": [{"id": "o3"}], "meta": {"limit": 2, "has_more": False, "next_cursor": None}} + ), + ) + ids = [org["id"] for org in env_client(fake).organizations.list_all({"limit": 2})] + + assert ids == ["o1", "o2", "o3"] + assert fake.calls[0].url == f"{API}/organizations?limit=2" + assert fake.calls[1].url == f"{API}/organizations?limit=2&after=c2" + + +def test_list_all_yields_typed_items() -> None: + fake = FakeApi(json_response({"data": [{"id": "o1"}], "meta": {"has_more": False}})) + + for org in env_client(fake).organizations.list_all(): + assert_type(org, Organization) + + +def test_follows_next_page_on_a_numbered_list_and_stops_when_has_more_is_false() -> None: + fake = FakeApi( + json_response( + {"data": [{"id": "m1"}], "meta": {"page": 1, "has_more": True, "next_page": 2}} + ), + json_response( + {"data": [{"id": "m2"}], "meta": {"page": 2, "has_more": False, "next_page": None}} + ), + ) + ws = WorkspaceClient(api_key="cbid_ws_k", transport=fake.transport) + ids = [member["id"] for member in ws.team.list_all()] + + assert ids == ["m1", "m2"] + assert fake.calls[0].url == "https://api.cboxid.com/api/v1/workspace/members" + assert fake.calls[1].url == "https://api.cboxid.com/api/v1/workspace/members?page=2" + + +def test_stops_reading_pages_when_the_caller_breaks_out() -> None: + fake = FakeApi( + json_response( + {"data": [{"id": "u1"}, {"id": "u2"}], "meta": {"has_more": True, "next_cursor": "x"}} + ) + ) + + for user in env_client(fake).users.list_all(): + assert user["id"] == "u1" + break + + assert len(fake.calls) == 1 + + +# ── Credentials ────────────────────────────────────────────────────────────────────────── + + +def test_refuses_a_key_from_the_wrong_plane_a_key_where_none_is_accepted_and_plain_http() -> None: + with pytest.raises(ConfigurationError): + EnvironmentClient(base_url=HOST, api_key="cbid_ws_x") + with pytest.raises(ConfigurationError): + WorkspaceClient(api_key="cbid_env_x") + with pytest.raises(ConfigurationError, match="accepts no management key"): + PlatformClient(api_key="cbid_ws_x") + with pytest.raises(ConfigurationError): + AccountClient(base_url=HOST, api_key="cbid_env_x") + with pytest.raises(ConfigurationError, match="https"): + EnvironmentClient(base_url="http://acme.test", api_key=KEY) + with pytest.raises(ConfigurationError, match="exactly one"): + EnvironmentClient(base_url=HOST) + with pytest.raises(ConfigurationError, match="base_url"): + EnvironmentClient(api_key=KEY) + + EnvironmentClient(base_url="http://localhost:8000", api_key=KEY).close() + + +def test_drives_any_environment_from_the_platform_root_with_one_token() -> None: + fake = FakeApi( + json_response({"data": []}), + held("apr_1", "https://api.cboxid.test/api/v1/action-approvals/apr_1"), + approval_status("approved"), + json_response({"data": APP}, 201), + ) + root = EnvironmentClient( + base_url="https://api.cboxid.test", + access_token="root_token", + environment="acme-staging", + transport=fake.transport, + approval_poll_interval=0, + ) + + root.apps.list() + root.apps.create({"name": "A"}) + + assert fake.calls[0].url == "https://api.cboxid.test/api/v1/apps" + assert len(fake.calls) == 4 + assert fake.calls[3].headers["cbox-approval"] == "apr_1" + assert all(c.headers["cbox-environment"] == "acme-staging" for c in fake.calls) + assert all(c.headers["authorization"] == "Bearer root_token" for c in fake.calls) + + +def test_a_key_never_sends_cbox_environment() -> None: + fake = FakeApi(json_response({"data": []})) + env_client(fake).apps.list() + + assert "cbox-environment" not in fake.calls[0].headers + + +def test_refuses_environment_with_a_key_or_on_another_plane() -> None: + with pytest.raises(ConfigurationError): + EnvironmentClient(base_url=HOST, api_key=KEY, environment="acme") + with pytest.raises(ConfigurationError, match="environment plane"): + WorkspaceClient(access_token="t", environment="acme") + + +def test_calls_a_token_provider_before_every_request() -> None: + fake = FakeApi(json_response({"data": {}}), json_response({"data": {}})) + tokens = iter(["tok_1", "tok_2"]) + client = AccountClient( + base_url=f"{HOST}/api/v1/", access_token=lambda: next(tokens), transport=fake.transport + ) + + client.request("GET", "/me/profile") + client.request("GET", "/me/profile") + + assert fake.calls[0].url == f"{API}/me/profile" + assert [c.headers["authorization"] for c in fake.calls] == ["Bearer tok_1", "Bearer tok_2"] + + +def test_extra_headers_cannot_override_authorization() -> None: + fake = FakeApi(json_response({"data": []})) + client = env_client(fake, headers={"User-Agent": "acme/1", "Authorization": "Bearer stolen"}) + + client.apps.list(headers={"X-Trace": "t1"}) + + assert fake.calls[0].headers["user-agent"] == "acme/1" + assert fake.calls[0].headers["x-trace"] == "t1" + assert fake.calls[0].headers["authorization"] == f"Bearer {KEY}" + + +def _b64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode() + + +def test_presents_a_dpop_bound_token_with_a_proof_that_pins_method_url_and_token() -> None: + signer = ES256DPoPSigner(generate_dpop_key()) + fake = FakeApi(json_response({"data": APP}, 201)) + client = EnvironmentClient( + base_url=HOST, access_token="at_123", dpop=signer, transport=fake.transport + ) + + client.apps.create({"name": "A"}) + + call = fake.calls[0] + assert call.headers["authorization"] == "DPoP at_123" + proof = call.headers["dpop"] + header = jwt.get_unverified_header(proof) + assert header["typ"] == "dpop+jwt" + key = jwt.PyJWK(header["jwk"], "ES256") + payload = jwt.decode(proof, key.key, algorithms=["ES256"]) + assert payload["htm"] == "POST" + assert payload["htu"] == f"{API}/apps" + assert payload["ath"] == _b64url(hashlib.sha256(b"at_123").digest()) + assert isinstance(payload["jti"], str) + + +def test_answers_a_dpop_nonce_challenge_once_with_the_nonce_in_the_next_proof() -> None: + signer = ES256DPoPSigner(generate_dpop_key()) + fake = FakeApi( + json_response( + {"error": "use_dpop_nonce", "error_description": "Nonce required."}, + 401, + {"DPoP-Nonce": "n-1"}, + ), + json_response({"data": []}), + ) + EnvironmentClient( + base_url=HOST, access_token="at", dpop=signer, transport=fake.transport + ).apps.list() + + first = jwt.decode(fake.calls[0].headers["dpop"], options={"verify_signature": False}) + second = jwt.decode(fake.calls[1].headers["dpop"], options={"verify_signature": False}) + assert "nonce" not in first + assert second["nonce"] == "n-1" + + +def test_closes_only_a_client_it_created() -> None: + mine = httpx.Client() + with EnvironmentClient(base_url=HOST, api_key=KEY, http_client=mine) as client: + assert isinstance(client, EnvironmentClient) + + assert not mine.is_closed + mine.close() + + +# ── Generated surface ──────────────────────────────────────────────────────────────────── + + +def test_names_methods_after_x_action_with_the_specs_method_path_scope_and_danger() -> None: + client = env_client(FakeApi()) + + assert callable(client.apps.create) + assert callable(client.apps.secrets.rotate) + assert callable(client.sso.connections.require_sso) + assert callable(client.sso.saml_metadata.import_) + assert callable(client.organizations.list_all) + assert callable(WorkspaceClient(api_key="cbid_ws_k").environments.create) + # The account and platform planes prefix every action with the plane; the client drops it. + assert callable(AccountClient(base_url=HOST, access_token="t").sessions.revoke_others) + assert callable(PlatformClient(access_token="t").workspaces.create) + + apps_create = ENVIRONMENT_OPERATIONS["apps.create"] + assert (apps_create.method, apps_create.path, apps_create.scope, apps_create.danger) == ( + "POST", + "/apps", + "apps:write", + "critical", + ) + rotate = ENVIRONMENT_OPERATIONS["apps.secrets.rotate"] + assert rotate.action == "apps.secrets.rotate" + assert rotate.operation_id == "apps_secrets_rotate" + assert (rotate.method, rotate.path, rotate.path_params) == ( + "POST", + "/apps/{id}/secrets", + ("id",), + ) + assert rotate.approval is True + assert ENVIRONMENT_OPERATIONS["organizations.list"].pagination == "cursor" + grant = ENVIRONMENT_OPERATIONS["members.roles.grant"] + assert grant.method == "PUT" + assert grant.path_params == ("organization_id", "user_id", "role_id") + environments = WORKSPACE_OPERATIONS["environments.create"] + assert (environments.path, environments.scope, environments.danger) == ( + "/workspace/environments", + "environments:write", + "critical", + ) + assert WORKSPACE_OPERATIONS["team.list"].pagination == "page" + events = ENVIRONMENT_OPERATIONS["audit_logs.events.create"] + assert (events.path, events.scope, events.danger) == ( + "/audit-logs/events", + "audit_logs:write", + "write", + ) + assert ENVIRONMENT_OPERATIONS["audit_logs.verify"].scope == "audit_logs:read" + + +def test_generated_code_is_exactly_what_the_generator_makes_of_the_vendored_specs() -> None: + """Regenerate with ``python -m scripts.generate_management``.""" + for path, result in generate_all().items(): + assert path.read_text("utf-8") == result.code, f"{path.name} is stale" + + +SNAPSHOT = Path(__file__).parent / "fixtures" / "management_surface.txt" + + +def _surface(prefix: str, namespace: object) -> list[str]: + lines: list[str] = [] + + for name, member in sorted(vars(type(namespace)).items()): + if name.startswith("_") or not inspect.isfunction(member): + continue + + signature = inspect.signature(member) + params: list[str] = [] + star = False + + for param in list(signature.parameters.values())[1:]: + if param.kind is param.KEYWORD_ONLY and not star: + params.append("*") + star = True + + params.append(param.name if param.default is param.empty else f"{param.name}=") + + returns = signature.return_annotation + lines.append(f"{prefix}.{name}({', '.join(params)}) -> {returns}") + + for name, child in sorted(vars(namespace).items()): + if not name.startswith("_"): + lines.extend(_surface(f"{prefix}.{name}", child)) + + return lines + + +def test_generated_surface_matches_the_snapshot() -> None: + """Every public method of every plane. Refresh with ``UPDATE_SNAPSHOTS=1 pytest``.""" + clients: list[tuple[str, ManagementClient]] = [ + ("environment", EnvironmentClient(base_url=HOST, api_key=KEY)), + ("workspace", WorkspaceClient(api_key="cbid_ws_k")), + ("platform", PlatformClient(access_token="t")), + ("account", AccountClient(base_url=HOST, access_token="t")), + ] + lines: list[str] = [] + + for plane, client in clients: + for name, namespace in sorted(vars(client).items()): + if name != "core": + lines.extend(_surface(f"{plane}.{name}", namespace)) + + surface = "\n".join(lines) + "\n" + + if os.environ.get("UPDATE_SNAPSHOTS"): + SNAPSHOT.write_text(surface, "utf-8") + + assert surface == SNAPSHOT.read_text("utf-8") + + +def test_a_call_with_a_plain_operation_spec_runs_through_the_same_core() -> None: + fake = FakeApi(json_response({"data": {"ok": True}})) + client = env_client(fake) + result: Any = client.core.call(ENVIRONMENT_OPERATIONS["apps.get"], ["app_1"]) + + assert isinstance(result, ApiResponse) + assert fake.calls[0].url == f"{API}/apps/app_1" From 71ecb6076c0fafdb64216a3d670687a0f944b7f2 Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 10:27:49 +0200 Subject: [PATCH 3/8] test(management): audit chain vector from the server, logger and exports --- tests/fixtures/audit_chain.json | 125 +++++++++++ tests/test_management_audit_logs.py | 316 ++++++++++++++++++++++++++++ 2 files changed, 441 insertions(+) create mode 100644 tests/fixtures/audit_chain.json create mode 100644 tests/test_management_audit_logs.py diff --git a/tests/fixtures/audit_chain.json b/tests/fixtures/audit_chain.json new file mode 100644 index 0000000..075d11b --- /dev/null +++ b/tests/fixtures/audit_chain.json @@ -0,0 +1,125 @@ +{ + "_source": "Computed by cbox-id integration\/wave-6 (e0cffae) AuditLogEventResource::document + AuditLogChains::hash with cboxdk\/laravel-audit-chain v0.1.0 CanonicalJson. data is the events as the API returns them; canonical is the exact string the server hashed.", + "data": [ + { + "id": "01k7aaaaaaaaaaaaaaaaaaaaaa", + "organization_id": "org_1", + "sequence": 1, + "action": "invoice.voided", + "occurred_at": "2026-10-08T09:15:30.123Z", + "actor": { + "id": "usr_1", + "type": "user", + "name": "\u00c5se \u00d8rsted", + "metadata": { + "role": "admin", + "Zeta": 1, + "alpha": { + "b": 2, + "a": "x\/y" + } + } + }, + "targets": [ + { + "id": "inv_9", + "type": "invoice", + "name": "Invoice \"9\" ", + "metadata": null + }, + { + "id": "c_2", + "type": "customer", + "name": null, + "metadata": null + } + ], + "context": { + "location": "203.0.113.7", + "user_agent": "Mozilla\/5.0 (X11) \u2028line\ttab" + }, + "metadata": { + "reason": "duplicate \u2014 refunded", + "amount": 1.5, + "ok": true, + "none": null, + "emoji": "\ud83d\ude42", + "url": "https:\/\/acme.test\/a\/b" + }, + "schema_version": null, + "received_at": "2026-10-08T09:20:00Z", + "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "hash": "25d461c942725293438357165fd03d850d7f8078fc5eae16779503e7a619a58f", + "canonical": "{\"action\":\"invoice.voided\",\"actor\":{\"id\":\"usr_1\",\"metadata\":{\"Zeta\":1,\"alpha\":{\"a\":\"x\/y\",\"b\":2},\"role\":\"admin\"},\"name\":\"\u00c5se \u00d8rsted\",\"type\":\"user\"},\"context\":{\"location\":\"203.0.113.7\",\"user_agent\":\"Mozilla\/5.0 (X11) \\u2028line\\ttab\"},\"id\":\"01k7aaaaaaaaaaaaaaaaaaaaaa\",\"metadata\":{\"amount\":1.5,\"emoji\":\"\ud83d\ude42\",\"none\":null,\"ok\":true,\"reason\":\"duplicate \u2014 refunded\",\"url\":\"https:\/\/acme.test\/a\/b\"},\"occurred_at\":\"2026-10-08T09:15:30.123Z\",\"organization_id\":\"org_1\",\"sequence\":1,\"targets\":[{\"id\":\"inv_9\",\"metadata\":null,\"name\":\"Invoice \\\"9\\\" \",\"type\":\"invoice\"},{\"id\":\"c_2\",\"metadata\":null,\"name\":null,\"type\":\"customer\"}]}" + }, + { + "id": "01k7bbbbbbbbbbbbbbbbbbbbbb", + "organization_id": "org_1", + "sequence": 2, + "action": "user.signed_in", + "occurred_at": "2026-10-08T09:16:00.000Z", + "actor": { + "id": "usr_2", + "type": "user", + "name": null, + "metadata": null + }, + "targets": [], + "context": { + "location": null, + "user_agent": null + }, + "metadata": null, + "schema_version": null, + "received_at": "2026-10-08T09:20:00Z", + "prev_hash": "25d461c942725293438357165fd03d850d7f8078fc5eae16779503e7a619a58f", + "hash": "868f6f132e577a33103bb872103abc1685d93729e68f87c4593715a3d78d44bd", + "canonical": "{\"action\":\"user.signed_in\",\"actor\":{\"id\":\"usr_2\",\"metadata\":null,\"name\":null,\"type\":\"user\"},\"context\":{\"location\":null,\"user_agent\":null},\"id\":\"01k7bbbbbbbbbbbbbbbbbbbbbb\",\"metadata\":null,\"occurred_at\":\"2026-10-08T09:16:00.000Z\",\"organization_id\":\"org_1\",\"sequence\":2,\"targets\":[]}" + }, + { + "id": "01k7cccccccccccccccccccccc", + "organization_id": "org_1", + "sequence": 3, + "action": "report.exported", + "occurred_at": "2026-10-08T09:17:00.500Z", + "actor": { + "id": "key_1", + "type": "api_key", + "name": "CI", + "metadata": { + "1": "b", + "0": "a" + } + }, + "targets": [ + { + "id": "r_1", + "type": "report", + "name": "Q3", + "metadata": { + "10": "k", + "2": "j" + } + } + ], + "context": { + "location": "::1", + "user_agent": "curl\/8" + }, + "metadata": { + "\u00fc": 1, + "u": 2, + "U": 3, + "_": 4, + "\ue000": 5, + "\ud83d\ude00": 6, + "ctl": "\u0001\u001f" + }, + "schema_version": null, + "received_at": "2026-10-08T09:20:00Z", + "prev_hash": "868f6f132e577a33103bb872103abc1685d93729e68f87c4593715a3d78d44bd", + "hash": "1b814b4124a9e06bbbb2aebedf5f08d34831b702303d2f58f1e971e1a61aa48a", + "canonical": "{\"action\":\"report.exported\",\"actor\":{\"id\":\"key_1\",\"metadata\":[\"a\",\"b\"],\"name\":\"CI\",\"type\":\"api_key\"},\"context\":{\"location\":\"::1\",\"user_agent\":\"curl\/8\"},\"id\":\"01k7cccccccccccccccccccccc\",\"metadata\":{\"U\":3,\"_\":4,\"ctl\":\"\\u0001\\u001f\",\"u\":2,\"\u00fc\":1,\"\ue000\":5,\"\ud83d\ude00\":6},\"occurred_at\":\"2026-10-08T09:17:00.500Z\",\"organization_id\":\"org_1\",\"sequence\":3,\"targets\":[{\"id\":\"r_1\",\"metadata\":{\"10\":\"k\",\"2\":\"j\"},\"name\":\"Q3\",\"type\":\"report\"}]}" + } + ] +} diff --git a/tests/test_management_audit_logs.py b/tests/test_management_audit_logs.py new file mode 100644 index 0000000..d5c60a9 --- /dev/null +++ b/tests/test_management_audit_logs.py @@ -0,0 +1,316 @@ +"""Audit Logs helpers: the chain verifier against the server's vector, the logger, exports.""" + +from __future__ import annotations + +import json +import re +import threading +from pathlib import Path +from typing import Any +from urllib.parse import parse_qs, urlsplit + +import httpx +import pytest + +from cbox_id.management import ( + AUDIT_CHAIN_GENESIS, + AuditChainVerification, + AuditLogExportError, + AuditLogger, + CboxIdApiError, + CboxIdError, + ConfigurationError, + RetryOptions, + audit_event_document, + audit_event_hash, + canonical_json, + export_audit_logs, + verify_audit_chain, + verify_audit_log_chain, +) + +from .management_helpers import API, Call, FakeApi, env_client, json_response + +FIXTURE = Path(__file__).parent / "fixtures" / "audit_chain.json" + + +def fixture() -> list[dict[str, Any]]: + data: list[dict[str, Any]] = json.loads(FIXTURE.read_text("utf-8"))["data"] + return data + + +def client(fake: FakeApi) -> Any: + return env_client(fake, retry=RetryOptions(max_retries=0)) + + +# ── Chain: byte for byte with the server ───────────────────────────────────────────────── + + +def test_canonicalises_and_hashes_each_event_exactly_as_cbox_id_did() -> None: + """The vector was computed by the server's own code (see the fixture's ``_source``).""" + previous = AUDIT_CHAIN_GENESIS + + for event in fixture(): + assert canonical_json(audit_event_document(event)) == event["canonical"] + assert event["prev_hash"] == previous + assert audit_event_hash(previous, event) == event["hash"] + previous = event["hash"] + + +def test_verifies_the_chain_in_any_order_and_finds_a_changed_missing_or_relinked_event() -> None: + events = fixture() + + assert verify_audit_chain(list(reversed(events))) == AuditChainVerification( + valid=True, + verified_count=3, + first_sequence=1, + last_sequence=3, + broken_at_sequence=None, + reason=None, + ) + + changed = [{**e, "action": "user.signed_out"} if e["sequence"] == 2 else e for e in events] + result = verify_audit_chain(changed) + assert (result.valid, result.reason, result.broken_at_sequence, result.verified_count) == ( + False, + "hash", + 2, + 1, + ) + + gap = verify_audit_chain([e for e in events if e["sequence"] != 2]) + assert (gap.valid, gap.reason, gap.broken_at_sequence) == (False, "missing", 2) + + relinked = [ + {**e, "prev_hash": AUDIT_CHAIN_GENESIS} if e["sequence"] == 3 else e for e in events + ] + link = verify_audit_chain(relinked) + assert (link.valid, link.reason, link.broken_at_sequence) == (False, "link", 3) + + # A window that starts later trusts its first prev_hash, unless told what it must be. + tail = [e for e in events if e["sequence"] > 1] + assert verify_audit_chain(tail).valid is True + assert verify_audit_chain(tail).first_sequence == 2 + assert verify_audit_chain(tail, previous_hash="f" * 64).reason == "link" + + assert verify_audit_chain([]) == AuditChainVerification(True, 0, None, None, None, None) + + +def test_writes_numbers_and_strings_the_way_php_json_encode_does() -> None: + # Expected strings are PHP 8's json_encode output for the same values (id-js's vector). + values = [1.0, 1e25, 1e-7, 0.1, 1.5, 1e15, 1e16, 1e17, 0.0001, 0.00012, 1.2345e20] + values += [-2.5e-5, 0.1 + 0.2, 1e100, -0.0] + assert [canonical_json(v) for v in values] == [ + "1", + "1.0e+25", + "1.0e-7", + "0.1", + "1.5", + "1000000000000000", + "10000000000000000", + "1.0e+17", + "0.0001", + "0.00012", + "1.2345e+20", + "-2.5e-5", + "0.30000000000000004", + "1.0e+100", + "-0", + ] + assert canonical_json({"b": "x/y", "a": 'Æ\u2028"\n'}) == '{"a":"Æ\\u2028\\"\\n","b":"x/y"}' + assert canonical_json({"1": "b", "0": "a"}) == '["a","b"]' + assert canonical_json({"10": "k", "2": "j"}) == '{"10":"k","2":"j"}' + assert canonical_json({}) == "[]" + assert canonical_json([]) == "[]" + assert canonical_json({"a": True, "b": None, "c": 7}) == '{"a":true,"b":null,"c":7}' + + with pytest.raises(ValueError): + canonical_json(float("nan")) + + +# ── AuditLogger ────────────────────────────────────────────────────────────────────────── + + +def accept(call: Call) -> httpx.Response: + return json_response({"data": {"events": (call.body or {}).get("events", [])}}, 201) + + +def test_sends_batches_of_at_most_100_each_under_its_own_idempotency_key() -> None: + fake = FakeApi(accept, accept, accept) + logger = AuditLogger(client(fake), flush_interval=0) + + for i in range(250): + logger.record( + { + "organization_id": "org_1", + "action": "invoice.viewed", + "actor": {"id": f"u{i}", "type": "user"}, + } + ) + logger.flush() + + assert [len(c.body["events"]) for c in fake.calls] == [100, 100, 50] + assert all(c.method == "POST" and c.url == f"{API}/audit-logs/events" for c in fake.calls) + assert len({c.headers["idempotency-key"] for c in fake.calls}) == 3 + first = fake.calls[0].body["events"][0] + assert re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$", first["occurred_at"]) + assert logger.pending == 0 + + +def test_keeps_a_failed_batch_and_resends_it_with_the_same_key() -> None: + fake = FakeApi(json_response({"error": "server_error", "message": "Down."}, 503), accept) + logger = AuditLogger(client(fake), flush_interval=0) + logger.record( + { + "organization_id": "org_1", + "action": "a.b", + "actor": {"id": "u", "type": "user"}, + "occurred_at": "2026-10-08T00:00:00.000Z", + } + ) + + with pytest.raises(CboxIdApiError) as caught: + logger.flush() + + assert caught.value.status == 503 + assert logger.pending == 1 + + logger.flush() + + assert len(fake.calls) == 2 + assert fake.calls[1].headers["idempotency-key"] == fake.calls[0].headers["idempotency-key"] + assert fake.calls[1].body["events"][0]["occurred_at"] == "2026-10-08T00:00:00.000Z" + assert logger.pending == 0 + + +def test_a_full_batch_without_a_thread_is_sent_from_record_and_failures_go_to_on_error() -> None: + fake = FakeApi(json_response({"error": "server_error", "message": "Down."}, 503), accept) + errors: list[tuple[Exception, int]] = [] + logger = AuditLogger( + client(fake), + batch_size=2, + flush_interval=0, + on_error=lambda exc, batch: errors.append((exc, len(batch))), + ) + event: Any = {"organization_id": "o", "action": "a.b", "actor": {"id": "u", "type": "user"}} + + logger.record(event) + assert fake.calls == [] + logger.record(event) + + assert len(fake.calls) == 1 + assert len(errors) == 1 and errors[0][1] == 2 + assert logger.pending == 2 + + logger.close() + assert logger.pending == 0 + + +def test_flushes_on_its_interval_and_refuses_events_once_closed() -> None: + sent = threading.Event() + + def answer(call: Call) -> httpx.Response: + sent.set() + return accept(call) + + fake = FakeApi(answer) + logger = AuditLogger(client(fake), flush_interval=0.05) + logger.record( + {"organization_id": "org_1", "action": "a.b", "actor": {"id": "u", "type": "user"}} + ) + + assert sent.wait(5), "the background thread never flushed" + logger.close() + + assert len(fake.calls) == 1 + with pytest.raises(CboxIdError, match="closed"): + logger.record( + {"organization_id": "org_1", "action": "a.b", "actor": {"id": "u", "type": "user"}} + ) + + +def test_refuses_a_batch_size_outside_1_to_100() -> None: + for size in (0, 101, True): + with pytest.raises(ConfigurationError): + AuditLogger(client(FakeApi()), batch_size=size, flush_interval=0) + + +# ── Exports and lists ──────────────────────────────────────────────────────────────────── + + +def exported(state: str) -> dict[str, Any]: + return { + "data": { + "id": "exp_1", + "organization_id": "org_1", + "state": state, + "filters": {}, + "row_count": None, + "url": "https://files.test/x.csv" if state == "ready" else None, + "created_at": None, + "completed_at": None, + "expires_at": None, + } + } + + +def test_creates_an_export_and_reads_it_until_it_is_ready() -> None: + fake = FakeApi( + json_response(exported("pending"), 201), + json_response(exported("pending")), + json_response(exported("pending")), + json_response(exported("ready")), + ) + result = export_audit_logs(client(fake), {"organization_id": "org_1"}, poll_interval=0) + + assert result["url"] == "https://files.test/x.csv" + assert [f"{c.method} {c.url}" for c in fake.calls] == [ + f"POST {API}/audit-logs/exports", + f"GET {API}/audit-logs/exports/exp_1", + f"GET {API}/audit-logs/exports/exp_1", + f"GET {API}/audit-logs/exports/exp_1", + ] + assert fake.calls[0].body == {"organization_id": "org_1"} + + +def test_raises_when_the_export_fails_or_is_not_ready_in_time() -> None: + failed = FakeApi(json_response(exported("pending"), 201), json_response(exported("failed"))) + + with pytest.raises(AuditLogExportError) as caught: + export_audit_logs(client(failed), poll_interval=0) + + assert caught.value.export is not None and caught.value.export["state"] == "failed" + + slow = FakeApi(json_response(exported("pending"), 201)) + + with pytest.raises(AuditLogExportError, match="in time"): + export_audit_logs(client(slow), poll_interval=0, timeout=0) + + +def test_pages_events_and_verifies_an_organizations_chain_from_the_list() -> None: + events = fixture() + + def page(call: Call) -> httpx.Response: + if "after" in parse_qs(urlsplit(call.url).query): + return json_response({"data": events[2:], "meta": {"has_more": False}}) + return json_response({"data": events[:2], "meta": {"has_more": True, "next_cursor": "c1"}}) + + fake = FakeApi(page, page, page, page) + env = client(fake) + ids = [ + event["id"] + for event in env.audit_logs.events.list_all( + {"organization_id": "org_1", "actions": ["invoice.voided", "user.signed_in"]} + ) + ] + + assert ids == [e["id"] for e in events] + assert fake.calls[0].url == ( + f"{API}/audit-logs/events?organization_id=org_1" + "&actions%5B%5D=invoice.voided&actions%5B%5D=user.signed_in" + ) + + result = verify_audit_log_chain(env, "org_1") + + assert (result.valid, result.verified_count) == (True, 3) + assert parse_qs(urlsplit(fake.calls[2].url).query)["order"] == ["asc"] From 3ed6d1226248777cce80300dcd5cc1c2aeb973ad Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 10:27:49 +0200 Subject: [PATCH 4/8] docs: Management API --- CHANGELOG.md | 44 +++++++++++ README.md | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 260 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e2bdb9..e7ec6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,50 @@ All notable changes to `cbox-id-client` are recorded here. Earlier releases are described by their tags and commit history. +## [Unreleased] + +### Added + +- `cbox_id.management`: typed clients for Cbox ID's management planes, generated from the + OpenAPI documents the server publishes. `EnvironmentClient` (an environment's own host, + `cbid_env_…` key or delegated token), `WorkspaceClient` (`cbid_ws_…` key), + `PlatformClient` (operator token) and `AccountClient` (a person's own token). Methods are + named after the server's actions (`env.apps.secrets.rotate(id, body)`, + `workspace.environments.create(body)`), with `TypedDict` bodies, queries and responses + that pass `mypy --strict`. Synchronous, on `httpx`, like the rest of the package. +- Every write sends an `Idempotency-Key` (a fresh UUID unless you pass one) and is retried + with the same key on network failures, `5xx`, `429` and `409 idempotency_in_progress`, + respecting `Retry-After`. `ApiResponse.replayed` reports `Idempotent-Replayed`. +- Approvals: a `202 approval_required` calls `on_approval_required` (show the binding code), + polls the approval on the plane's own host only, and repeats the request with + `Cbox-Approval` and the same key. `ApprovalDeniedError` and `ApprovalExpiredError` when it + is not approved; `approval="return"` hands back a `PendingApprovalResult` with `resume()`. +- `CboxIdApiError` (`status`, `error`, `message`, `errors`, `request_id` from the envelope's + `request_id` or `X-Request-Id`, `retry_after`) and `ManagementNetworkError` (with the + `idempotency_key` to repeat safely). +- `environment=` on `EnvironmentClient`: with a person's root access token and the platform + root as `base_url`, it sends `Cbox-Environment` so one token can drive any environment of + the workspace. +- Audit Logs helpers: `AuditLogger` (buffers events and sends batches of up to 100, each + with its own Idempotency-Key, on size, interval and `flush()`), `export_audit_logs()` + (creates an export and polls it until ready), and `verify_audit_chain()` / + `verify_audit_log_chain()` with `canonical_json()` and `audit_event_hash()`, which + recompute an organization's hash chain byte for byte as the server does — checked against + a vector the server's own code computed. +- `…_all` iterators on every paged list (cursor and page-number paging). +- DPoP-bound access tokens: `ES256DPoPSigner` and `generate_dpop_key()`, with + `DPoP-Nonce` challenge handling. +- Operation tables (`ENVIRONMENT_OPERATIONS`, …) with each action's method, path, scope, + danger and whether it can be held for approval. +- `python -m scripts.generate_management` regenerates the clients from the vendored specs in + `openapi/`; `--fetch =` refreshes a spec from a running server first, and + `--check` fails when the generated code is stale. So does the test suite. + +### Changed + +- New runtime dependency: `typing-extensions>=4.7` (`NotRequired` and `TypedDict` on + Python 3.10). The `dev` extra adds `pyyaml` and `types-PyYAML` for the generator. + ## [0.9.0] - 2026-09-24 Organization selection, support sessions, and staff roles. Needs a Cbox ID instance that diff --git a/README.md b/README.md index d38a4f4..3bd4796 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,219 @@ ok = verify_webhook( ) ``` +## Management API + +`cbox_id.management` is a typed client for Cbox ID's management planes. It is generated from +the OpenAPI documents the server publishes, so every method, path, scope and type matches the +server it was generated from. Use it from server code only: every client holds a management +credential. + +| Client | Plane | Credential | `base_url` | +| --- | --- | --- | --- | +| `EnvironmentClient` | One environment's tenancy: organizations, users, apps, roles, SSO, audit logs… | `cbid_env_…` key, or a delegated access token | The environment's own host, or the platform root with `environment` | +| `WorkspaceClient` | The workspace above its environments: projects, environments, team, keys | `cbid_ws_…` key, or a person's root token | `https://api.cboxid.com` (default) | +| `PlatformClient` | The deployment itself, for operators | Delegated operator token only | `https://api.cboxid.com` (default) | +| `AccountClient` | A person's own account | Delegated token only | The environment's own host | + +Method names are the server's action names: `apps.secrets.rotate` is +`env.apps.secrets.rotate(...)`, and `sso.saml_metadata.import` is +`env.sso.saml_metadata.import_(...)`. Path parameters come first, in path order, then the body +(or the query, for a read) as a dict, then keyword-only options. Every call returns an +`ApiResponse` with `data`, `meta`, `body`, `status`, `replayed`, `idempotency_key`, +`request_id` and `headers`. Bodies, queries and `data` are `TypedDict`s, so mypy and your +editor check the keys. + +The client is synchronous, on `httpx`, like the rest of this package. + +### Create an app and rotate its secret + +```python +import os + +from cbox_id.management import ApprovalDeniedError, CboxIdApiError, EnvironmentClient + +env = EnvironmentClient( + base_url="https://acme.cboxid.com", + api_key=os.environ["CBOX_ID_ENV_KEY"], # cbid_env_… + on_approval_required=lambda approval, ctx: print( + f"Approve {ctx.action} on your device. Code: {approval.binding_code}" + ), +) + +app = env.apps.create( + {"name": "Billing", "type": "web", "redirect_uris": ["https://billing.acme.com/callback"]} +).data +# app["client_secret"] is in this response and in no other. Store it now. + +try: + secret = env.apps.secrets.rotate(app["id"], {"grace_seconds": 3600}).data + # secret["client_secret"]: the new secret, shown once. +except ApprovalDeniedError: + print("Rotation was declined.") +except CboxIdApiError as exc: + if not exc.is_validation_error: + raise + print(exc.errors) +``` + +When a key's policy holds an action for a person's approval, the server answers +`202 approval_required`. By default the client calls `on_approval_required` (show the +`binding_code` so the person can match it on their device), polls the approval, and repeats +the request with `Cbox-Approval: ` and the same `Idempotency-Key` once it is approved. +`ApprovalDeniedError` and `ApprovalExpiredError` are raised when it is denied or expires. The +poll goes only to the plane's own host: a `poll_url` on another origin is refused rather than +handed the credential. If you do not want to wait in the same call, pass `approval="return"`: + +```python +from cbox_id.management import PendingApprovalResult + +outcome = env.apps.secrets.rotate(app["id"], {"grace_seconds": 0}, approval="return") +if isinstance(outcome, PendingApprovalResult): + print(f"Code: {outcome.approval.binding_code}") + secret = outcome.resume().data # polls, then repeats the request +``` + +### Bootstrap an environment from a workspace key + +```python +from cbox_id.management import EnvironmentClient, WorkspaceClient + +workspace = WorkspaceClient(api_key=os.environ["CBOX_ID_WS_KEY"]) # cbid_ws_… + +created = workspace.environments.create( + { + "name": "Staging", + "type": "sandbox", + "initial_key": {"name": "bootstrap", "scopes": ["apps:write", "organizations:write"]}, + } +).data + +# The first management key, returned once. On an idempotent replay its token is None. +initial_key = created["initial_key"] +assert initial_key is not None and initial_key["token"] is not None +env = EnvironmentClient(base_url=created["issuer"], api_key=initial_key["token"]) +env.organizations.create({"name": "Acme", "slug": "acme"}) +``` + +### Idempotency, retries and errors + +- Every `POST`, `PUT`, `PATCH` and `DELETE` sends an `Idempotency-Key`, a fresh UUID unless + you pass `idempotency_key=`. Network failures, `5xx`, `429` and + `409 idempotency_in_progress` are retried with the **same** key + (`retry=RetryOptions(max_retries=3, base_delay=0.5, max_delay=30.0)`, in seconds). + `Retry-After` is respected; one longer than `max_delay` is raised instead of waited out. + `replayed` is `True` when the server returned the first request's stored answer + (`Idempotent-Replayed`). A secret in a replayed answer is `None`. +- A failed call raises `CboxIdApiError` with `status`, `error` (the stable code), `message`, + `errors` (field-keyed, on `validation_failed`), `request_id` (the envelope's `request_id`, + else the `X-Request-Id` header; quote it when reporting a problem) and `retry_after`. A call + that never got an answer raises `ManagementNetworkError`, which carries the + `idempotency_key` so you can repeat the request safely. Both are `CboxIdError`s. +- The client never logs. Secrets in responses are returned as they arrive, and request + bodies are never put in an error. + +### One token for every environment + +A person's access token issued at the platform root reaches the workspace, account and +operator planes there, and any environment of their workspace when the request names it with +`Cbox-Environment`. Pass `environment` (an id or slug) and the root host as `base_url`: + +```python +staging = EnvironmentClient( + base_url="https://api.cboxid.com", + access_token=tokens.current, # a str, or a callable called before every request + environment="acme-staging", # sent as Cbox-Environment on every request +) +staging.organizations.portal_links.create(org_id, {"intents": ["sso", "dsync"]}) +``` + +What the token may do there is bounded by the person's role and the token's scopes. A +`cbid_env_…` key is bound to its own environment's host, so `environment` is refused with one. + +### Lists + +Every paged list also has a `…_all` variant that iterates every item, fetching pages as you +reach them. It follows `meta.next_cursor` on the environment plane and `meta.next_page` on the +workspace plane: + +```python +for org in env.organizations.list_all({"limit": 100}): + print(org["id"], org["name"]) +``` + +### Audit Logs + +Your app records what its users did, per organization (your customer), and Cbox ID keeps +each organization's events in a tamper-evident hash chain. `AuditLogger` buffers events and +sends them in batches of up to 100, each batch under its own `Idempotency-Key`. It sends a +batch when it is full, every `flush_interval` seconds (default 5, from a daemon thread), and on +`flush()` / `close()`. A batch that fails stays queued with the same key, so sending it again +never records an event twice: + +```python +from collections.abc import Sequence + +from cbox_id.management import AuditLogEventInput, AuditLogger + +def report(exc: Exception, batch: Sequence[AuditLogEventInput]) -> None: + log.warning("audit flush failed (%d events kept for the next one): %s", len(batch), exc) + + +with AuditLogger(env, on_error=report) as audit: + audit.record( + { + "organization_id": org["id"], + "action": "invoice.voided", + "actor": {"id": user.id, "type": "user", "name": user.name}, + "targets": [{"id": invoice.id, "type": "invoice"}], + "context": {"location": request.remote_addr, "user_agent": request.user_agent.string}, + "metadata": {"reason": "duplicate"}, + } + ) # occurred_at defaults to now +# Leaving the block (or audit.close()) stops the thread and sends what is left. +``` + +Read events with `env.audit_logs.events.list_all({"organization_id": …, "actions": […]})`. +To get a CSV, `export_audit_logs(env, filters)` starts an export and polls it until it is +ready. Its `url` is signed and short-lived. To check the chain yourself instead of trusting +the server's `env.audit_logs.verify()`, call `verify_audit_log_chain(env, organization_id)` or +`verify_audit_chain(events)`. They recompute `sha256(prev_hash + canonical JSON)` exactly as +the server does — keys sorted by UTF-8 bytes at every depth, PHP's list and empty-object +rules, PHP's float format, slashes and Unicode unescaped — and report +`AuditChainVerification(valid, reason, broken_at_sequence, …)`. + +### Delegated tokens and DPoP + +Pass `access_token` (a string, or a callable called before every request so it can refresh) +instead of `api_key`. For a DPoP-bound token, pass a signer built from the P-256 key the token +was bound to: + +```python +from cbox_id.management import AccountClient, ES256DPoPSigner + +me = AccountClient( + base_url="https://acme.cboxid.com", + access_token=tokens.current, + dpop=ES256DPoPSigner(private_key), # an EllipticCurvePrivateKey (cryptography) +) +me.sessions.revoke_others() +``` + +### Types, metadata and regenerating + +Schema and operation types live in each plane's module (`cbox_id.management.environment_api.App`, +`cbox_id.management.workspace_api.EnvironmentsCreateBody`). The operation tables +(`ENVIRONMENT_OPERATIONS`, …) list each action's method, path, scope, danger and whether it can +be held for approval, which is useful for showing a confirmation before a `critical` action. +`env.request(method, path, query=…, body=…)` calls a route that is not generated. + +The specs are vendored in `openapi/`. `python -m scripts.generate_management` (with the `dev` +extra installed) regenerates `src/cbox_id/management/generated/` from them, and +`python -m scripts.generate_management --fetch environment=https://acme.cboxid.com` (or +`workspace=`, `platform=`, `account=`, `all=`) refreshes a vendored spec from a running server +first. `--check` exits non-zero when the generated code is stale, and the test suite fails +when the generated code and the vendored specs disagree. + ## Security & scope Login is hardened by default — PKCE, `state`, nonce, and full `id_token` verification @@ -353,9 +566,9 @@ so `alg:none` and algorithm confusion are both refused. Keep the client secret and webhook secrets server-side. This is a **client**. It authenticates users — into a chosen organization, when you ask — -and calls a Cbox ID instance's standard endpoints; it does not configure SSO, run SCIM, or -administer organizations and their members — those are platform capabilities of -[`cboxdk/laravel-id`](https://github.com/cboxdk/laravel-id). +and calls a Cbox ID instance's endpoints. The management client drives the server's own +management API with a credential you hold; SSO, SCIM and the rules behind every action stay +platform capabilities of [`cboxdk/laravel-id`](https://github.com/cboxdk/laravel-id). Report vulnerabilities via this repo's GitHub **Private Vulnerability Reporting**. From 30aa82cf3dc1a365b8a32f7f5653bd45d732ab69 Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 15:11:55 +0200 Subject: [PATCH 5/8] feat(management): adopt the wave-7 specs Typed answers (ManagementKey with a nullable token on replay, LegacyLoginProbe, ManifestSync, SelfServiceSignup, VerificationResend), webhook signature_scheme and webhooks.signature_scheme.change, log stream options and health, the log_streams.test failure kind, and organizations.portal_links.list/revoke. More routes now declare 202 approval_required, so their methods take approval=. --- openapi/environment.yaml | 980 ++++++++++++++++-- openapi/workspace.yaml | 102 +- .../management/generated/environment.py | 549 ++++++++-- src/cbox_id/management/generated/workspace.py | 164 ++- tests/fixtures/management_surface.txt | 57 +- tests/test_management.py | 22 + 6 files changed, 1609 insertions(+), 265 deletions(-) diff --git a/openapi/environment.yaml b/openapi/environment.yaml index 9b6c7bc..39a36c0 100644 --- a/openapi/environment.yaml +++ b/openapi/environment.yaml @@ -82,7 +82,7 @@ tags: - name: Invitations - name: Roles - name: Users - - name: Apps + - name: Applications - name: APIs - name: 'API keys' - name: 'Support sessions' @@ -90,18 +90,18 @@ tags: - name: Approvals - name: Governance - name: 'Audit log' - - name: 'Audit Logs' + - name: 'App audit logs' - name: Branding - name: 'Directory Sync' - name: Domains - name: Events - - name: 'Frontend keys' + - name: 'Publishable keys' - name: Hooks - - name: 'Management keys' + - name: 'Secret keys' - name: 'Legacy login' - name: 'Log streams' - name: 'Admin Portal' - - name: Provisioning + - name: 'Outbound provisioning' - name: 'SAML apps' - name: Sign-in - name: 'Enterprise SSO' @@ -722,6 +722,7 @@ paths: parameters: - $ref: '#/components/parameters/Id' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -754,6 +755,7 @@ paths: parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -777,6 +779,8 @@ paths: $ref: '#/components/schemas/Api' meta: $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -793,6 +797,7 @@ paths: summary: 'Register an API and its scopes' parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -847,6 +852,8 @@ paths: properties: data: $ref: '#/components/schemas/Api' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -869,6 +876,7 @@ paths: description: 'Requires scope `apis:read`.' parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -887,6 +895,8 @@ paths: properties: data: $ref: '#/components/schemas/Api' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -908,6 +918,7 @@ paths: register a new API instead. parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -944,6 +955,8 @@ paths: properties: data: $ref: '#/components/schemas/Api' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -965,6 +978,7 @@ paths: expire; apps holding its scope keys keep them as plain scopes. parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -972,6 +986,8 @@ paths: - WorkspaceAccessToken: [] responses: + 202: + $ref: '#/components/responses/ApprovalRequired' 204: description: Deleted. 400: @@ -1242,7 +1258,7 @@ paths: /apps: get: tags: - - Apps + - Applications summary: 'List apps' description: 'Requires scope `apps:read`. Never a secret.' operationId: apps_list @@ -1250,6 +1266,7 @@ paths: parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -1273,6 +1290,8 @@ paths: $ref: '#/components/schemas/App' meta: $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -1285,7 +1304,7 @@ paths: $ref: '#/components/responses/TooManyRequests' post: tags: - - Apps + - Applications summary: 'Register an app' description: | Requires scope `apps:write`. Two ways to describe it: @@ -1309,6 +1328,7 @@ paths: x-action: apps.create parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -1392,6 +1412,8 @@ paths: properties: data: $ref: '#/components/schemas/App' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -1407,7 +1429,7 @@ paths: '/apps/{id}': delete: tags: - - Apps + - Applications summary: 'Delete an app and every secret it holds. Anything signing in as it stops working immediately.' description: 'Requires scope `apps:write`. Danger: critical.' operationId: apps_delete @@ -1453,7 +1475,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - Apps + - Applications summary: 'Get one app (OAuth client) by its id or its client_id: its kind, grants, redirect URIs, scopes and settings. Never a secret.' description: 'Requires scope `apps:read`. Danger: read.' operationId: apps_get @@ -1503,7 +1525,7 @@ paths: $ref: '#/components/responses/TooManyRequests' patch: tags: - - Apps + - Applications summary: 'Rename an app and replace its redirect URIs or sign-out redirect URIs. Fields left out are unchanged.' description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_update @@ -1584,7 +1606,7 @@ paths: '/apps/{id}/blueprint': get: tags: - - Apps + - Applications summary: "Export an app's configuration as a blueprint" description: | Requires scope `apps:read`. `{id}` is the app's id or its `client_id`. The document @@ -1595,6 +1617,7 @@ paths: x-action: apps.blueprint parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -1613,6 +1636,8 @@ paths: properties: data: $ref: '#/components/schemas/AppBlueprint' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -1626,7 +1651,7 @@ paths: '/apps/{id}/copy': post: tags: - - Apps + - Applications summary: 'Copy an app into another environment of this project, with a new client id and secret there. A person on the environment console only; a key exports the blueprint instead.' description: 'Requires scope `apps:write`. Danger: critical.' operationId: apps_copy @@ -1706,7 +1731,7 @@ paths: '/apps/{id}/manifest': put: tags: - - Apps + - Applications summary: 'Set or clear the URL an app publishes its roles-and-permissions manifest at. Does not fetch it; apps.manifest.sync does.' description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_manifest_set @@ -1776,7 +1801,7 @@ paths: '/apps/{id}/manifest/sync': post: tags: - - Apps + - Applications summary: "Fetch an app's published manifest now and sync the roles and permissions it declares." description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_manifest_sync @@ -1812,7 +1837,7 @@ paths: - data properties: data: - type: object + $ref: '#/components/schemas/ManifestSync' 202: $ref: '#/components/responses/ApprovalRequired' 400: @@ -1832,7 +1857,7 @@ paths: '/apps/{id}/scopes': put: tags: - - Apps + - Applications summary: "Replace an app's complete scope set — the ceiling of what it may request, applied from its next token." description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_scopes_set @@ -1902,7 +1927,7 @@ paths: '/apps/{id}/secrets': get: tags: - - Apps + - Applications summary: "List an app's live client secrets" description: | Requires scope `apps:read`. Every live secret, newest first: its id (what @@ -1913,6 +1938,7 @@ paths: x-action: apps.secrets.list parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -1936,6 +1962,8 @@ paths: $ref: '#/components/schemas/AppSecret' meta: $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' 400: $ref: '#/components/responses/EnvironmentRequired' 401: @@ -1948,7 +1976,7 @@ paths: $ref: '#/components/responses/TooManyRequests' post: tags: - - Apps + - Applications summary: 'Mint a new client secret for an app and retire the current ones after grace_seconds (0 = at once). The new secret is returned once.' description: 'Requires scope `apps:write`. Danger: critical.' operationId: apps_secrets_rotate @@ -2018,7 +2046,7 @@ paths: '/apps/{id}/secrets/{secret_id}': delete: tags: - - Apps + - Applications summary: "Revoke one of an app's client secrets immediately. Never its last live secret — rotate that instead." description: 'Requires scope `apps:write`. Danger: critical.' operationId: apps_secrets_revoke @@ -2073,7 +2101,7 @@ paths: '/apps/{id}/settings/api-key-prefix': put: tags: - - Apps + - Applications summary: "Set the prefix of an app's customer API keys (acme_live), which lets its users create keys for it, or null to stop new keys." description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_settings_api_key_prefix @@ -2142,7 +2170,7 @@ paths: '/apps/{id}/settings/backchannel-logout': put: tags: - - Apps + - Applications summary: 'Set or clear the URI an app is sent a logout token at when somebody signs out (OIDC Back-Channel Logout).' description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_settings_backchannel_logout @@ -2215,7 +2243,7 @@ paths: '/apps/{id}/settings/token-exchange': put: tags: - - Apps + - Applications summary: 'Turn token exchange (RFC 8693) on or off for a confidential app.' description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_settings_token_exchange @@ -2282,7 +2310,7 @@ paths: '/apps/{id}/settings/token-lifetime': put: tags: - - Apps + - Applications summary: "Set how long an app's access tokens live, in seconds, or null for the install's default." description: 'Requires scope `apps:write`. Danger: write.' operationId: apps_settings_token_lifetime @@ -2444,7 +2472,7 @@ paths: /audit-logs/events: post: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Record 1–100 audit events your app's users caused, each for one of your organizations (customers); checked against the action's schema when it has one and appended to that organization's tamper-evident chain. Send an Idempotency-Key." description: 'Requires scope `audit_logs:write`. Danger: write.' operationId: audit_logs_events_create @@ -2593,7 +2621,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - 'Audit Logs' + - 'App audit logs' summary: 'Read audit events newest first, for one organization or the whole environment, filtered by action, actor, target and time range; pass `next_cursor` as `after` to page.' description: 'Requires scope `audit_logs:read`. Danger: read.' operationId: audit_logs_events_list @@ -2720,7 +2748,7 @@ paths: /audit-logs/exports: post: tags: - - 'Audit Logs' + - 'App audit logs' summary: 'Start a CSV export of audit events (same filters as the list). It is written on the queue: poll audit_logs.exports.get until state is ready, then download from its short-lived url.' description: 'Requires scope `audit_logs:export`. Danger: write.' operationId: audit_logs_exports_create @@ -2803,7 +2831,7 @@ paths: '/audit-logs/exports/{id}': get: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Read an audit-log export's state; once ready it carries a signed download url valid for a few minutes (read it again for a fresh one)." description: 'Requires scope `audit_logs:read`. Danger: read.' operationId: audit_logs_exports_get @@ -2857,7 +2885,7 @@ paths: /audit-logs/schemas: post: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Define the schema one audit-log action's events must match — allowed target types and metadata schemas (a subset of JSON Schema). Events of that action are then validated on arrival." description: 'Requires scope `audit_logs:manage`. Danger: write.' operationId: audit_logs_schemas_create @@ -2943,7 +2971,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - 'Audit Logs' + - 'App audit logs' summary: 'List the audit-log schemas this environment validates events against, one per action.' description: 'Requires scope `audit_logs:read`. Danger: read.' operationId: audit_logs_schemas_list @@ -3006,7 +3034,7 @@ paths: '/audit-logs/schemas/{action}': delete: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Delete an audit-log action's schema. Its events are then accepted unchecked — or refused, if the environment is in strict mode." description: 'Requires scope `audit_logs:manage`. Danger: destructive.' operationId: audit_logs_schemas_delete @@ -3051,7 +3079,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Read the schema one audit-log action's events are validated against." description: 'Requires scope `audit_logs:read`. Danger: read.' operationId: audit_logs_schemas_get @@ -3102,7 +3130,7 @@ paths: $ref: '#/components/responses/TooManyRequests' put: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Replace an audit-log action's schema with a new version (omitted parts are removed). Recorded events keep the version they were checked against." description: 'Requires scope `audit_logs:manage`. Danger: write.' operationId: audit_logs_schemas_update @@ -3191,7 +3219,7 @@ paths: /audit-logs/settings: get: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Read this environment's audit-log settings: retention in days and whether strict schemas are on." description: 'Requires scope `audit_logs:read`. Danger: read.' operationId: audit_logs_settings_get @@ -3234,7 +3262,7 @@ paths: $ref: '#/components/responses/TooManyRequests' patch: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Change this environment's audit-log retention (days, 1–3650) and strict mode. Shortening retention deletes older events at the next daily prune — irreversibly." description: 'Requires scope `audit_logs:manage`. Danger: destructive.' operationId: audit_logs_settings_update @@ -3296,7 +3324,7 @@ paths: /audit-logs/verify: get: tags: - - 'Audit Logs' + - 'App audit logs' summary: "Re-hash one organization's audit-event chain (from the oldest event retention kept, or from_sequence) and report whether every event is unchanged and in place." description: 'Requires scope `audit_logs:read`. Danger: read.' operationId: audit_logs_verify @@ -4860,7 +4888,7 @@ paths: /frontend-keys: post: tags: - - 'Frontend keys' + - 'Publishable keys' summary: 'Create a publishable frontend key for a browser app, usable only from the origins listed. The key is public by design.' description: 'Requires scope `frontend_keys:write`. Danger: write.' operationId: frontend_keys_create @@ -4935,7 +4963,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - 'Frontend keys' + - 'Publishable keys' summary: 'List the publishable frontend keys browser apps present to the Frontend API, in full, with their allowed origins.' description: 'Requires scope `frontend_keys:read`. Danger: read.' operationId: frontend_keys_list @@ -4998,7 +5026,7 @@ paths: '/frontend-keys/{id}': delete: tags: - - 'Frontend keys' + - 'Publishable keys' summary: 'Revoke a publishable frontend key. Pages still holding it stop working immediately.' description: 'Requires scope `frontend_keys:write`. Danger: destructive.' operationId: frontend_keys_revoke @@ -5043,7 +5071,7 @@ paths: '/frontend-keys/{id}/origins': put: tags: - - 'Frontend keys' + - 'Publishable keys' summary: 'Replace the exact list of origins allowed to present a publishable frontend key. Live on the next request.' description: 'Requires scope `frontend_keys:write`. Danger: write.' operationId: frontend_keys_set_origins @@ -5427,7 +5455,7 @@ paths: /keys: post: tags: - - 'Management keys' + - 'Secret keys' summary: 'Mint a management key for this environment, at most as wide as the caller. The value is returned once, as `token`.' description: 'Requires scope `keys:write`. Danger: critical.' operationId: keys_create @@ -5507,7 +5535,7 @@ paths: - data properties: data: - type: object + $ref: '#/components/schemas/ManagementKey' 202: $ref: '#/components/responses/ApprovalRequired' 400: @@ -5526,7 +5554,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - 'Management keys' + - 'Secret keys' summary: "List this environment's management keys (names, scopes, parents, expiry, last use — never their values)." description: 'Requires scope `keys:read`. Danger: read.' operationId: keys_list @@ -5569,7 +5597,7 @@ paths: data: type: array items: - type: object + $ref: '#/components/schemas/ManagementKey' meta: $ref: '#/components/schemas/PageMeta' 202: @@ -5589,7 +5617,7 @@ paths: '/keys/{id}': delete: tags: - - 'Management keys' + - 'Secret keys' summary: 'Revoke a management key and every key it minted, immediately.' description: 'Requires scope `keys:write`. Danger: destructive.' operationId: keys_revoke @@ -5634,7 +5662,7 @@ paths: '/keys/{id}/rotate': post: tags: - - 'Management keys' + - 'Secret keys' summary: 'Rotate a management key: mint a successor with the same scopes and retire the old one after a grace period.' description: 'Requires scope `keys:write`. Danger: critical.' operationId: keys_rotate @@ -5680,7 +5708,7 @@ paths: - data properties: data: - type: object + $ref: '#/components/schemas/ManagementKey' 202: $ref: '#/components/responses/ApprovalRequired' 400: @@ -5832,7 +5860,7 @@ paths: - data properties: data: - type: object + $ref: '#/components/schemas/LegacyLoginProbe' 202: $ref: '#/components/responses/ApprovalRequired' 400: @@ -5900,7 +5928,7 @@ paths: post: tags: - 'Log streams' - summary: 'Stream the audit trail to a SIEM (Splunk, Elastic, Graylog, CEF, JSON). A generated HMAC key is returned once.' + summary: 'Stream the audit trail to a SIEM (Splunk, Elastic, Graylog, CEF, JSON), Datadog, or an S3 or GCS bucket. A generated HMAC key is returned once.' description: 'Requires scope `log_streams:write`. Danger: critical.' operationId: log_streams_create x-action: log_streams.create @@ -5935,14 +5963,19 @@ paths: - graylog_gelf - cef_http - generic_json + - datadog + - s3 + - gcs endpoint_url: - type: string - description: 'A public URL the entries are POSTed to.' + type: + - string + - 'null' + description: "An HTTP collector: the public URL entries are POSTed to (required). Datadog, S3 and GCS: leave out for the destination's own endpoint, or an https URL of an S3-compatible store (MinIO, R2)." format: uri maxLength: 2048 auth: type: string - description: "How the endpoint is authenticated. Left out, the destination's default." + description: "An HTTP collector: how the endpoint is authenticated. Left out, the destination's default. The cloud destinations authenticate their own way." enum: - none - bearer @@ -5952,8 +5985,106 @@ paths: type: - string - 'null' - description: 'The bearer or Splunk token the endpoint expects. Left out with `hmac`, a key is generated and returned once.' - maxLength: 4096 + description: 'The credential, never echoed: the bearer or Splunk token (HTTP collectors; left out with `hmac`, a key is generated and returned once), the Datadog API key, the S3 secret access key (none with `role_arn`), or the GCS service-account JSON key.' + maxLength: 8192 + options: + type: object + description: "Datadog, S3 and GCS only: the destination's settings. An HTTP collector takes none." + properties: + site: + type: + - string + - 'null' + description: 'Datadog: the site your account lives on — the domain you sign in at, e.g. `datadoghq.eu` for EU1. Default `datadoghq.com` (US1). An API key only works on its own site.' + enum: + - datadoghq.com + - us3.datadoghq.com + - us5.datadoghq.com + - datadoghq.eu + - ap1.datadoghq.com + - ap2.datadoghq.com + - ddog-gov.com + service: + type: + - string + - 'null' + description: "Datadog: the `service` attribute on every entry. Default: this platform's name." + maxLength: 100 + source: + type: + - string + - 'null' + description: 'Datadog: the `ddsource` attribute. Default `cbox`.' + maxLength: 100 + tags: + type: + - array + - 'null' + description: 'Datadog: `key:value` tags (`ddtags`) on every entry, e.g. `env:prod`.' + maxItems: 100 + items: + type: string + maxLength: 200 + hostname: + type: + - string + - 'null' + description: "Datadog: the `hostname` attribute. Default: this platform's host." + maxLength: 255 + bucket: + type: + - string + - 'null' + description: 'S3 and GCS: the bucket objects are written to.' + maxLength: 222 + region: + type: + - string + - 'null' + description: "S3: the bucket's AWS Region, e.g. `eu-west-1` (`auto` for Cloudflare R2)." + maxLength: 32 + prefix: + type: + - string + - 'null' + description: 'S3 and GCS: the object key prefix, e.g. `cbox/audit`. Objects are written to `{prefix}/{yyyy}/{mm}/{dd}/{hh}/{batch}.ndjson.gz`.' + maxLength: 512 + access_key_id: + type: + - string + - 'null' + description: "S3 with an access key: the IAM access key ID. Its secret access key is the stream's `secret`." + maxLength: 128 + role_arn: + type: + - string + - 'null' + description: "S3 with an assumed role: the IAM role the platform assumes. No secret is stored; the role's trust policy must require the stream's `external_id`." + maxLength: 2048 + sse: + type: + - string + - 'null' + description: "S3: server-side encryption requested on every object. Left out, the bucket's default." + enum: + - AES256 + - 'aws:kms' + kms_key_id: + type: + - string + - 'null' + description: 'S3 with `aws:kms`: the KMS key ID, alias or ARN.' + maxLength: 2048 + path_style: + type: + - boolean + - 'null' + description: 'S3: path-style addressing. Default: virtual-hosted on AWS, path-style on a custom endpoint (MinIO, R2).' + gzip: + type: + - boolean + - 'null' + description: 'S3 and GCS: gzip each object (`.ndjson.gz`). Default true.' organization_id: type: - string @@ -5966,7 +6097,6 @@ paths: required: - name - destination - - endpoint_url responses: 201: description: OK @@ -6161,7 +6291,7 @@ paths: patch: tags: - 'Log streams' - summary: 'Disable (enabled: false) or resume (enabled: true) an audit log stream. Disabled, entries are kept and delivered on resume.' + summary: "Change an audit log stream's name, destination, endpoint, options or credential (re-validated; resets its circuit breaker), or disable (enabled: false) / resume (enabled: true) it." description: 'Requires scope `log_streams:write`. Danger: critical.' operationId: log_streams_update x-action: log_streams.update @@ -6186,17 +6316,149 @@ paths: - WorkspaceAccessToken: [] requestBody: - required: true + required: false content: application/json: schema: type: object properties: + name: + type: string + maxLength: 190 + destination: + type: string + description: "Changing it starts the stream's options and credential afresh." + enum: + - splunk_hec + - elastic_ecs + - graylog_gelf + - cef_http + - generic_json + - datadog + - s3 + - gcs + endpoint_url: + type: + - string + - 'null' + description: "An HTTP collector's URL; for Datadog, S3 and GCS empty means the destination's own endpoint." + format: uri + maxLength: 2048 + auth: + type: string + description: 'An HTTP collector only.' + enum: + - none + - bearer + - splunk + - hmac + secret: + type: + - string + - 'null' + description: 'A new credential (token, API key, secret access key or service-account JSON key). Left out, the current one is kept. Never echoed.' + maxLength: 8192 + options: + type: object + description: "Datadog, S3 and GCS only: the destination's settings. An HTTP collector takes none." + properties: + site: + type: + - string + - 'null' + description: 'Datadog: the site your account lives on — the domain you sign in at, e.g. `datadoghq.eu` for EU1. Default `datadoghq.com` (US1). An API key only works on its own site.' + enum: + - datadoghq.com + - us3.datadoghq.com + - us5.datadoghq.com + - datadoghq.eu + - ap1.datadoghq.com + - ap2.datadoghq.com + - ddog-gov.com + service: + type: + - string + - 'null' + description: "Datadog: the `service` attribute on every entry. Default: this platform's name." + maxLength: 100 + source: + type: + - string + - 'null' + description: 'Datadog: the `ddsource` attribute. Default `cbox`.' + maxLength: 100 + tags: + type: + - array + - 'null' + description: 'Datadog: `key:value` tags (`ddtags`) on every entry, e.g. `env:prod`.' + maxItems: 100 + items: + type: string + maxLength: 200 + hostname: + type: + - string + - 'null' + description: "Datadog: the `hostname` attribute. Default: this platform's host." + maxLength: 255 + bucket: + type: + - string + - 'null' + description: 'S3 and GCS: the bucket objects are written to.' + maxLength: 222 + region: + type: + - string + - 'null' + description: "S3: the bucket's AWS Region, e.g. `eu-west-1` (`auto` for Cloudflare R2)." + maxLength: 32 + prefix: + type: + - string + - 'null' + description: 'S3 and GCS: the object key prefix, e.g. `cbox/audit`. Objects are written to `{prefix}/{yyyy}/{mm}/{dd}/{hh}/{batch}.ndjson.gz`.' + maxLength: 512 + access_key_id: + type: + - string + - 'null' + description: "S3 with an access key: the IAM access key ID. Its secret access key is the stream's `secret`." + maxLength: 128 + role_arn: + type: + - string + - 'null' + description: "S3 with an assumed role: the IAM role the platform assumes. No secret is stored; the role's trust policy must require the stream's `external_id`." + maxLength: 2048 + sse: + type: + - string + - 'null' + description: "S3: server-side encryption requested on every object. Left out, the bucket's default." + enum: + - AES256 + - 'aws:kms' + kms_key_id: + type: + - string + - 'null' + description: 'S3 with `aws:kms`: the KMS key ID, alias or ARN.' + maxLength: 2048 + path_style: + type: + - boolean + - 'null' + description: 'S3: path-style addressing. Default: virtual-hosted on AWS, path-style on a custom endpoint (MinIO, R2).' + gzip: + type: + - boolean + - 'null' + description: 'S3 and GCS: gzip each object (`.ndjson.gz`). Default true.' enabled: type: boolean description: 'True to deliver; false to stop, keeping what is pending.' - required: - - enabled responses: 200: description: OK @@ -6229,7 +6491,7 @@ paths: post: tags: - 'Log streams' - summary: 'Send one test entry to a log stream now and report whether the SIEM accepted it.' + summary: 'Send one test event to a log stream now and report whether the destination accepted it, and if not, why.' description: 'Requires scope `log_streams:write`. Danger: write.' operationId: log_streams_test x-action: log_streams.test @@ -6371,6 +6633,7 @@ paths: - active - suspended - deleted + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6414,6 +6677,7 @@ paths: description: 'Requires scope `organizations:write`.' parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6505,6 +6769,7 @@ paths: description: 'Requires scope `organizations:read`.' parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6547,6 +6812,7 @@ paths: parameters: - $ref: '#/components/parameters/Id' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6622,6 +6888,7 @@ paths: parameters: - $ref: '#/components/parameters/Id' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6782,6 +7049,7 @@ paths: parameters: - $ref: '#/components/parameters/Id' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6849,6 +7117,7 @@ paths: required: false schema: type: string + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -6896,6 +7165,7 @@ paths: that proves it (`record_name`, `record_value`). A handful at most; not paged. parameters: - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7199,6 +7469,7 @@ paths: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7263,6 +7534,7 @@ paths: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7350,6 +7622,7 @@ paths: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/InvitationId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7394,6 +7667,7 @@ paths: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/InvitationId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7442,6 +7716,7 @@ paths: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/After' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7502,6 +7777,7 @@ paths: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7586,6 +7862,7 @@ paths: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/UserId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7646,6 +7923,7 @@ paths: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/UserId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7685,6 +7963,7 @@ paths: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/UserId' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7742,6 +8021,7 @@ paths: - $ref: '#/components/parameters/RoleId' - $ref: '#/components/parameters/RoleClientId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7791,6 +8071,7 @@ paths: - $ref: '#/components/parameters/RoleId' - $ref: '#/components/parameters/RoleClientId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -7815,6 +8096,55 @@ paths: 429: $ref: '#/components/responses/TooManyRequests' '/organizations/{organization_id}/portal-links': + get: + operationId: organizations_portal_links_list + x-action: organizations.portal_links.list + x-scope: 'portal_links:read' + x-danger: read + tags: + - 'Admin Portal' + summary: "List an organization's Admin Portal links" + description: | + Requires scope `portal_links:read`. Danger: read. The links minted in the last 30 + days, newest first — every link that can still be opened is among them, since a link + waits a week at most. Each says what it opens, who minted it, whom it was mailed to + and where it stands (`status`). Never the link itself: it was shown once, when it was + minted, and only its hash is kept. Not paged. + parameters: + - $ref: '#/components/parameters/OrganizationId' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + WorkspaceAccessToken: [] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: '#/components/schemas/PortalLinkRecord' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 429: + $ref: '#/components/responses/TooManyRequests' post: tags: - 'Admin Portal' @@ -7919,17 +8249,33 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' - /permissions: - post: + '/organizations/{organization_id}/portal-links/{id}': + delete: tags: - - Roles - summary: "Author a manual `feature:action` permission, shared with the environment (organization_id null) or one organization's own." - description: 'Requires scope `role_definitions:write`. Danger: write.' - operationId: permissions_create - x-action: permissions.create - x-scope: 'role_definitions:write' - x-danger: write + - 'Admin Portal' + summary: 'Withdraw an Admin Portal link: it can no longer be opened, and a setup session it already opened ends on its next request. What was already set up through it stays.' + description: 'Requires scope `portal_links:write`. Danger: destructive.' + operationId: organizations_portal_links_revoke + x-action: organizations.portal_links.revoke + x-scope: 'portal_links:write' + x-danger: destructive parameters: + - + name: organization_id + in: path + required: true + schema: + type: string + description: 'The organization id.' + maxLength: 64 + - + name: id + in: path + required: true + schema: + type: string + description: "The portal link's id." + maxLength: 64 - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' @@ -7940,8 +8286,48 @@ paths: ManagementAccessToken: [] - WorkspaceAccessToken: [] - requestBody: - required: true + responses: + 204: + description: Done. + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' + /permissions: + post: + tags: + - Roles + summary: "Author a manual `feature:action` permission, shared with the environment (organization_id null) or one organization's own." + description: 'Requires scope `role_definitions:write`. Danger: write.' + operationId: permissions_create + x-action: permissions.create + x-scope: 'role_definitions:write' + x-danger: write + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true content: application/json: schema: @@ -8203,7 +8589,7 @@ paths: /provisioning-targets: post: tags: - - Provisioning + - 'Outbound provisioning' summary: "Register a downstream app this platform pushes an organization's people to over SCIM, or every organization's with environment_wide." description: 'Requires scope `provisioning:write`. Danger: critical.' operationId: provisioning_targets_create @@ -8310,7 +8696,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - Provisioning + - 'Outbound provisioning' summary: 'List the downstream SCIM targets people are provisioned to, optionally for one organization, with their failures. Never their credentials.' description: 'Requires scope `provisioning:read`. Danger: read.' operationId: provisioning_targets_list @@ -8383,7 +8769,7 @@ paths: '/provisioning-targets/{id}': delete: tags: - - Provisioning + - 'Outbound provisioning' summary: 'Delete a downstream SCIM target. Nothing more is pushed to it.' description: 'Requires scope `provisioning:write`. Danger: destructive.' operationId: provisioning_targets_delete @@ -8442,7 +8828,7 @@ paths: $ref: '#/components/responses/TooManyRequests' get: tags: - - Provisioning + - 'Outbound provisioning' summary: 'Read one downstream SCIM target: its URL, auth scheme, status and last error. Never its credential.' description: 'Requires scope `provisioning:read`. Danger: read.' operationId: provisioning_targets_get @@ -8503,7 +8889,7 @@ paths: '/provisioning-targets/{id}/status': post: tags: - - Provisioning + - 'Outbound provisioning' summary: 'Pause or resume pushing people to a downstream SCIM target.' description: 'Requires scope `provisioning:write`. Danger: write.' operationId: provisioning_targets_status_set @@ -8598,6 +8984,7 @@ paths: required: false schema: type: string + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -9648,7 +10035,7 @@ paths: - data properties: data: - type: object + $ref: '#/components/schemas/SelfServiceSignup' 202: $ref: '#/components/responses/ApprovalRequired' 400: @@ -9769,7 +10156,7 @@ paths: maxLength: 64 provider: type: string - description: 'The catalogue key: google, microsoft, okta, auth0, keycloak, gitlab, slack, github, discord, apple, facebook.' + description: 'The catalogue key: google, microsoft, okta, auth0, keycloak, gitlab, slack, github, discord, apple, facebook, linkedin, bitbucket, xero, intuit.' maxLength: 100 client_id: type: string @@ -10376,7 +10763,7 @@ paths: type: - string - 'null' - description: 'OIDC: the signing key. Write-only.' + description: "OIDC, optional: the provider's ID-token signing key (an RS256 public key, PEM). Leave it out for any provider whose discovery document publishes a jwks_uri — the keys are read from there and follow its rotations. Needed only when it does not. Write-only." maxLength: 20000 required: - name @@ -10700,7 +11087,7 @@ paths: type: - string - 'null' - description: 'OIDC: the signing key. Write-only.' + description: "OIDC, optional: the provider's ID-token signing key (an RS256 public key, PEM). Leave it out for any provider whose discovery document publishes a jwks_uri — the keys are read from there and follow its rotations. Needed only when it does not. Write-only." maxLength: 20000 responses: 200: @@ -11592,6 +11979,7 @@ paths: one token and are refused before the session starts (`422 invalid_target`). parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12256,6 +12644,7 @@ paths: - active - disabled - locked + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12299,6 +12688,7 @@ paths: description: 'Requires scope `users:write`. Password is optional — omit it to create a passwordless identity the user completes via an invite/magic-link.' parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12368,6 +12758,7 @@ paths: description: 'Requires scope `users:read`.' parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12410,6 +12801,7 @@ paths: parameters: - $ref: '#/components/parameters/Id' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12519,6 +12911,7 @@ paths: description: 'Requires scope `roles:read`.' parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12563,6 +12956,7 @@ paths: - $ref: '#/components/parameters/Id' - $ref: '#/components/parameters/RoleId' - $ref: '#/components/parameters/RoleClientId' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12611,6 +13005,7 @@ paths: - $ref: '#/components/parameters/RoleId' - $ref: '#/components/parameters/RoleClientId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12657,6 +13052,7 @@ paths: - $ref: '#/components/parameters/RoleId' - $ref: '#/components/parameters/RoleClientId' - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -12983,6 +13379,7 @@ paths: `impersonation` marks a session somebody else opened as this person. parameters: - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/CboxApproval' - $ref: '#/components/parameters/CboxEnvironment' security: - @@ -13582,6 +13979,12 @@ paths: - vault.grant.revoked - vault.secret.revoked - governance.access.revoked + signature_scheme: + type: string + description: 'How deliveries are signed: `cbox` (X-Cbox-Signature, the default) or `standard_webhooks` (webhook-id / webhook-timestamp / webhook-signature, verifiable with any Standard Webhooks library; the secret is a `whsec_` secret).' + enum: + - cbox + - standard_webhooks organization_id: type: - string @@ -14026,6 +14429,77 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + '/webhooks/{id}/signature-scheme': + post: + tags: + - Webhooks + summary: "Change how a webhook endpoint's deliveries are signed (cbox or standard_webhooks). No new secret is issued: a hex secret is used as whsec_ + base64 of itself. Update the receiver first." + description: 'Requires scope `webhooks:write`. Danger: destructive.' + operationId: webhooks_signature_scheme_change + x-action: webhooks.signature_scheme.change + x-scope: 'webhooks:write' + x-danger: destructive + parameters: + - + name: id + in: path + required: true + schema: + type: string + description: 'The webhook endpoint id.' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' + - $ref: '#/components/parameters/CboxEnvironment' + security: + - + EnvironmentApiKey: [] + - + ManagementAccessToken: [] + - + WorkspaceAccessToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + signature_scheme: + type: string + description: 'The scheme deliveries are signed with from the next attempt. The secret is unchanged: under `standard_webhooks` a 64-hex secret is used as `whsec_` + base64 of the hex string. Update the receiver before you switch.' + enum: + - cbox + - standard_webhooks + required: + - signature_scheme + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Webhook' + 202: + $ref: '#/components/responses/ApprovalRequired' + 400: + $ref: '#/components/responses/EnvironmentRequired' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/Conflict' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 429: + $ref: '#/components/responses/TooManyRequests' components: securitySchemes: EnvironmentApiKey: @@ -14196,6 +14670,149 @@ components: schema: type: string schemas: + ManagementKey: + type: object + description: 'A management key of this environment. `token` is present once, on the answer that minted it, and never again.' + required: + - id + - name + - prefix + - scopes + - active + - created_at + properties: + id: + type: string + name: + type: string + description: + type: + - string + - 'null' + prefix: + type: string + description: "The key's first characters, to recognise it by." + scopes: + type: array + items: + type: string + active: + type: boolean + parent_key_id: + type: + - string + - 'null' + description: 'The key that minted this one, if a key did.' + rotated_from_id: + type: + - string + - 'null' + description: 'The key this one replaced, if it came from a rotation.' + require_approval: + description: 'The approval policy: actions held for the person behind the key.' + oneOf: + - + type: object + properties: + min_danger: + type: + - string + - 'null' + enum: + - read + - write + - destructive + - critical + - null + actions: + type: array + items: + type: string + - type: 'null' + created_by: + oneOf: + - + type: object + required: + - type + - id + properties: + type: + type: string + id: + type: + - string + - 'null' + - type: 'null' + expires_at: + type: + - string + - 'null' + format: date-time + last_used_at: + type: + - string + - 'null' + format: date-time + revoked_at: + type: + - string + - 'null' + format: date-time + created_at: + type: + - string + - 'null' + format: date-time + token: + type: + - string + - 'null' + description: "The key's value. Only on the answer that minted it; null on an idempotent replay of that answer, because the value is never stored." + LegacyLoginProbe: + type: object + required: + - result + properties: + result: + type: string + description: 'What the declared legacy-login endpoint answered for the address, in words.' + ManifestSync: + type: object + required: + - unchanged + - roles_declared + - permissions_declared + - orphaned_role_keys + - orphaned_permission_keys + properties: + unchanged: + type: boolean + description: 'The manifest matched what is already stored.' + roles_declared: + type: integer + permissions_declared: + type: integer + orphaned_role_keys: + type: array + items: + type: string + description: 'Roles stored here that the manifest no longer declares.' + orphaned_permission_keys: + type: array + items: + type: string + SelfServiceSignup: + type: object + required: + - enabled + - decided_here + properties: + enabled: + type: boolean + decided_here: + type: boolean + description: 'False on a single-tenant install, where sign-up follows CBOX_ID_SIGNUP_MODE and the switch changes nothing.' Device: type: object required: @@ -14857,6 +15474,7 @@ components: - organization_id - event_types - active + - signature_scheme properties: id: type: string @@ -14874,6 +15492,17 @@ components: active: type: boolean description: 'false while paused.' + signature_scheme: + type: string + enum: + - cbox + - standard_webhooks + description: | + How deliveries are signed. `cbox`: `X-Cbox-Timestamp` and + `X-Cbox-Signature: t=,v1=.">`. + `standard_webhooks`: `webhook-id`, `webhook-timestamp` and + `webhook-signature: v1,..">`, verifiable + with any Standard Webhooks library. Only the chosen scheme's headers are sent. consecutive_failures: type: integer last_success_at: @@ -14892,7 +15521,8 @@ components: - 'null' description: | The signing secret, on the create and rotate answers only — shown once, never - retrievable again. `null` on an idempotent replay of that answer. + retrievable again. `null` on an idempotent replay of that answer. 64 hex + characters under `cbox`; a `whsec_` secret under `standard_webhooks`. InlineHook: type: object required: @@ -14940,8 +15570,11 @@ components: - destination - endpoint_url - auth + - options + - external_id - organization_id - enabled + - health properties: id: type: string @@ -14955,8 +15588,12 @@ components: - graylog_gelf - cef_http - generic_json + - datadog + - s3 + - gcs endpoint_url: type: string + description: 'Where entries go. For Datadog, S3 and GCS, derived from the site or region unless a custom endpoint was given.' auth: type: string enum: @@ -14964,6 +15601,60 @@ components: - bearer - splunk - hmac + description: "An HTTP collector's scheme; `none` for the cloud destinations, which authenticate their own way." + options: + type: + - object + - 'null' + description: "Datadog, S3 and GCS: the destination's settings, as stored. Never a credential — the API key, secret access key or service-account key is the encrypted secret. null for an HTTP collector." + properties: + site: + type: string + enum: + - datadoghq.com + - us3.datadoghq.com + - us5.datadoghq.com + - datadoghq.eu + - ap1.datadoghq.com + - ap2.datadoghq.com + - ddog-gov.com + service: + type: string + source: + type: string + hostname: + type: string + tags: + type: string + description: 'Comma-separated `key:value` tags (`ddtags`).' + bucket: + type: string + region: + type: string + prefix: + type: string + access_key_id: + type: string + role_arn: + type: string + external_id: + type: string + sse: + type: string + enum: + - AES256 + - 'aws:kms' + kms_key_id: + type: string + path_style: + type: boolean + gzip: + type: boolean + external_id: + type: + - string + - 'null' + description: "An assumed-role S3 stream's external ID, generated for it: the role's trust policy must require it (`sts:ExternalId`). null otherwise." organization_id: type: - string @@ -14971,6 +15662,14 @@ components: description: "null when the environment owns it: it carries EVERY organization's entries." enabled: type: boolean + health: + type: string + enum: + - healthy + - degraded + - paused + - action_required + description: '`degraded`: recent transient failures, still retrying. `paused`: the circuit is open after repeated failures and resumes on its own. `action_required`: the destination refused the credential or the settings — update the stream, or run a successful test.' consecutive_failures: type: integer last_success_at: @@ -14978,6 +15677,25 @@ components: - string - 'null' format: date-time + last_error: + type: + - string + - 'null' + description: "The latest failure, scrubbed of the stream's secret." + last_failure_kind: + type: + - string + - 'null' + enum: + - transient + - authentication + - configuration + - null + last_failure_at: + type: + - string + - 'null' + format: date-time created_at: type: - string @@ -14987,7 +15705,7 @@ components: type: - string - 'null' - description: 'A generated HMAC key, on the create answer only — shown once. `null` on an idempotent replay.' + description: 'A generated HMAC key, on the answer that generated it only — shown once. `null` on an idempotent replay. A credential the caller supplied is never echoed.' DomainEvent: type: object required: @@ -15819,13 +16537,17 @@ components: type: - object - 'null' - description: "What to paste into the identity provider: this connection's own entity id and ACS URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection." + description: "What to paste into the identity provider: this connection's own entity id, ACS URL and SP metadata URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection." properties: sp_entity_id: type: string sp_acs_url: type: string format: uri + sp_metadata_url: + type: string + format: uri + description: "SAML: this connection's service-provider metadata (entity id and ACS URL as one XML document), for an identity provider that imports SP metadata. Served for a draft too, before the identity provider's half is known." redirect_uri: type: string format: uri @@ -15943,6 +16665,7 @@ components: required: - id - delivered + - failure - error - tested_at properties: @@ -15950,7 +16673,17 @@ components: type: string delivered: type: boolean - description: 'Whether the destination accepted the test entry.' + description: 'Whether the destination accepted the test event (action `siem.stream.test`).' + failure: + type: + - string + - 'null' + enum: + - transient + - authentication + - configuration + - null + description: 'Why not: `transient` (unreachable, busy — retrying can help) or `authentication` / `configuration` (somebody has to fix the stream). null when delivered.' error: type: - string @@ -16062,6 +16795,87 @@ components: - string - 'null' format: date-time + PortalLinkRecord: + type: object + description: 'An Admin Portal link as a list shows it. Never its URL: that was shown once, when it was minted, and only its hash is kept.' + required: + - id + - organization_id + - intents + - status + - created_at + - created_by + - emailed_to + - expires_at + - consumed_at + - completed_at + - revoked_at + properties: + id: + type: string + organization_id: + type: string + intents: + type: array + description: 'What the link may set up.' + items: + type: string + enum: + - sso + - dsync + - domain_verification + - log_streams + - certificate_renewal + - audit_logs + status: + type: string + enum: + - pending + - in_use + - completed + - expired + - revoked + description: | + `pending`: not opened yet, and it still can be. `in_use`: opened, and the setup + session it started may still be running. `completed`: its setup was finished. + `expired`: never opened in time, or its setup session ran out. `revoked`: withdrawn. + created_at: + type: + - string + - 'null' + format: date-time + created_by: + type: string + description: "Who minted it: the console person's id, or the management key's." + emailed_to: + type: + - string + - 'null' + description: 'The address it was mailed to, or null when it was not sent.' + expires_at: + type: + - string + - 'null' + format: date-time + description: 'How long it could wait to be opened.' + consumed_at: + type: + - string + - 'null' + format: date-time + description: 'When it was opened. It is single-use.' + completed_at: + type: + - string + - 'null' + format: date-time + description: 'When its setup was finished.' + revoked_at: + type: + - string + - 'null' + format: date-time + description: 'When it was withdrawn.' AuditLogEvent: type: object description: | diff --git a/openapi/workspace.yaml b/openapi/workspace.yaml index 130b2ff..78356d3 100644 --- a/openapi/workspace.yaml +++ b/openapi/workspace.yaml @@ -5,7 +5,7 @@ info: title: 'Cbox ID — Workspace Management API' version: 1.0.0 description: | - The **global organization-management plane** — above every environment. Manage the + The **workspace plane** — the customer's own Cbox account, above every environment it owns. Manage the workspace, its **projects** (IdP products, each a billing anchor) and their environments, its team, and its keys with a workspace key. @@ -36,7 +36,7 @@ info: This is distinct from the per-environment API (OIDC, organizations, users, SCIM), which is served on each environment's own host and authenticated with - environment-scoped credentials. Credentials never cross planes: an organization key + environment-scoped credentials. Credentials never cross planes: a workspace key (`cbid_ws_…`) cannot call the environment API, and vice versa. **Envelope.** Every success returns `{ "data": … }` (an object, or an array with @@ -48,7 +48,7 @@ info: runner behind a shared NAT gets its own allowance. A `429` carries `Retry-After`. servers: - url: 'https://api.cboxid.com/api/v1' - description: 'Hosted (global organization plane)' + description: 'Hosted (the platform root)' - url: 'https://{host}/api/v1' description: Self-hosted @@ -61,10 +61,10 @@ security: - WorkspaceAccessToken: [] tags: - - name: Organization + - name: Workspace - name: Projects - name: Environments - - name: Members + - name: Team - name: Keys description: "Workspace keys, and the management keys of the workspace's environments." - name: Approvals @@ -73,11 +73,13 @@ paths: /workspace: get: tags: - - Organization - summary: 'Get the organization' + - Workspace + summary: 'Get the workspace' operationId: workspace_get x-action: workspace.get - description: "Requires scope `workspace:read` (any role). Returns the organization's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer)." + description: "Requires scope `workspace:read` (any role). Returns the workspace's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer)." + parameters: + - $ref: '#/components/parameters/CboxApproval' responses: 200: description: OK @@ -88,6 +90,8 @@ paths: properties: data: $ref: '#/components/schemas/Organization' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -98,7 +102,7 @@ paths: $ref: '#/components/responses/TooManyRequests' patch: tags: - - Organization + - Workspace summary: 'Rename the workspace.' description: 'Requires scope `settings:write` and a role that may `manage-members`. Danger: write.' operationId: workspace_settings_update @@ -215,6 +219,7 @@ paths: type: integer minimum: 1 default: 1 + - $ref: '#/components/parameters/CboxApproval' responses: 200: description: OK @@ -229,6 +234,8 @@ paths: $ref: '#/components/schemas/Environment' meta: $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -253,6 +260,7 @@ paths: x-action: environments.create parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' requestBody: required: true content: @@ -273,7 +281,7 @@ paths: default: production project_id: type: string - description: "The project (billing anchor) to create the environment under. Defaults to the organization's first project when omitted." + description: "The project (billing anchor) to create the environment under. Defaults to the workspace's first project when omitted." initial_key: type: - object @@ -307,12 +315,14 @@ paths: properties: data: $ref: '#/components/schemas/CreatedEnvironment' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 404: - description: 'No such project on this organization' + description: 'No such project in this workspace' content: application/json: schema: @@ -588,11 +598,13 @@ paths: /workspace/invitations: get: tags: - - Members + - Team summary: 'List pending invitations' operationId: team_invitations_list x-action: team.invitations.list description: "Requires scope `team:read` and the `read-members` capability. The team's invitations nobody has accepted yet, newest first (at most 100)." + parameters: + - $ref: '#/components/parameters/CboxApproval' responses: 200: description: OK @@ -605,6 +617,8 @@ paths: type: array items: $ref: '#/components/schemas/TeamInvitation' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -614,9 +628,9 @@ paths: '/workspace/invitations/{id}': delete: tags: - - Members + - Team summary: 'Withdraw an invitation' - description: 'Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending on this organization is a `404`.' + description: 'Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending in this workspace is a `404`.' operationId: team_invitations_revoke x-action: team.invitations.revoke parameters: @@ -627,7 +641,10 @@ paths: schema: type: string - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' responses: + 202: + $ref: '#/components/responses/ApprovalRequired' 204: description: Withdrawn 401: @@ -641,7 +658,7 @@ paths: '/workspace/invitations/{id}/resend': post: tags: - - Members + - Team summary: 'Re-send an invitation' description: | Requires scope `team:write` and the `manage-members` capability. Mails a fresh link; the earlier link stops @@ -657,6 +674,7 @@ paths: schema: type: string - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' responses: 200: description: 'Sent again' @@ -667,6 +685,8 @@ paths: properties: data: $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -816,7 +836,7 @@ paths: delete: tags: - Keys - summary: 'Revoke a workspace key, and every key it minted; whatever uses them stops immediately.' + summary: 'Revoke a workspace key, and every key it minted on either plane (workspace keys and environment management keys, all the way down); whatever uses them stops immediately.' description: 'Requires scope `keys:write` and a role that may `manage-environments` and `manage-members`. Danger: destructive.' operationId: keys_workspace_revoke x-action: keys.workspace.revoke @@ -851,7 +871,7 @@ paths: /workspace/members: get: tags: - - Members + - Team summary: 'List members' operationId: team_list x-action: team.list @@ -872,6 +892,7 @@ paths: type: integer minimum: 1 default: 1 + - $ref: '#/components/parameters/CboxApproval' responses: 200: description: OK @@ -886,6 +907,8 @@ paths: $ref: '#/components/schemas/Member' meta: $ref: '#/components/schemas/PageMeta' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -896,11 +919,11 @@ paths: $ref: '#/components/responses/TooManyRequests' post: tags: - - Members + - Team summary: 'Invite a member' description: | Requires scope `team:write` and the `manage-members` capability (owner/admin). Invites the address onto the - organization's team — the same invitation the console's Team page sends: a mail naming + workspace's team — the same invitation the console's Team page sends: a mail naming this key as the inviter and the role, with a signed link on which the invitee sets a password and is signed in. Owner is never invited; ownership is transferred. An earlier pending invitation for the same address is superseded. List, re-send and @@ -909,6 +932,7 @@ paths: x-action: team.invite parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' requestBody: required: true content: @@ -943,6 +967,8 @@ paths: properties: data: $ref: '#/components/schemas/Member' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -965,7 +991,7 @@ paths: '/workspace/members/{id}': delete: tags: - - Members + - Team summary: "Remove a member from the workspace's team. The owner is never removed; transfer ownership first." description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: destructive.' operationId: team_remove @@ -1001,7 +1027,7 @@ paths: '/workspace/members/{id}/access': put: tags: - - Members + - Team summary: "Set which of the workspace's environments a team member reaches: all of them, or the listed ones." description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: write.' operationId: team_environment_access @@ -1064,7 +1090,7 @@ paths: '/workspace/members/{id}/role': patch: tags: - - Members + - Team summary: "Change a team member's role (admin, developer, member or viewer). The owner's role changes only by transferring ownership." description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: write.' operationId: team_role @@ -1125,7 +1151,7 @@ paths: '/workspace/members/{id}/transfer-ownership': post: tags: - - Members + - Team summary: "Make another member the workspace's owner; the current owner stays on as an admin. Only the owner, signed in, may do this." description: 'Requires scope `team:write` and a role that may `manage-members`. Danger: critical.' operationId: team_transfer_ownership @@ -1165,7 +1191,9 @@ paths: summary: 'List projects' operationId: projects_list x-action: projects.list - description: "Requires scope `workspace:read` (any role). The organization's projects (IdP products). Each carries its own plan and environment allowance." + description: "Requires scope `workspace:read` (any role). The workspace's projects (IdP products). Each carries its own plan and environment allowance." + parameters: + - $ref: '#/components/parameters/CboxApproval' responses: 200: description: OK @@ -1178,6 +1206,8 @@ paths: type: array items: $ref: '#/components/schemas/Project' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -1195,6 +1225,7 @@ paths: x-action: projects.create parameters: - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CboxApproval' requestBody: required: true content: @@ -1222,6 +1253,8 @@ paths: properties: data: $ref: '#/components/schemas/Project' + 202: + $ref: '#/components/responses/ApprovalRequired' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -1256,7 +1289,7 @@ paths: - data properties: data: - type: object + $ref: '#/components/schemas/VerificationResend' 202: $ref: '#/components/responses/ApprovalRequired' 401: @@ -1489,7 +1522,7 @@ components: type: string format: uri Unauthorized: - description: 'Missing or invalid organization API key (or its organization is suspended)' + description: 'Missing or invalid workspace key (or its workspace is suspended)' content: application/json: schema: @@ -1507,7 +1540,7 @@ components: schema: $ref: '#/components/schemas/Error' NotFound: - description: 'No such resource on this organization' + description: 'No such resource in this workspace' content: application/json: schema: @@ -1543,6 +1576,19 @@ components: schema: $ref: '#/components/schemas/Error' schemas: + VerificationResend: + type: object + required: + - outcome + - message + properties: + outcome: + type: string + enum: + - sent + - already_provisioned + message: + type: string Organization: type: object properties: @@ -1557,7 +1603,7 @@ components: - suspended projects: type: array - description: "The organization's projects with each one's plan/allowance. Present only for billing-readers (plans are per project)." + description: "The workspace's projects with each one's plan/allowance. Present only for billing-readers (plans are per project)." items: $ref: '#/components/schemas/Project' Project: diff --git a/src/cbox_id/management/generated/environment.py b/src/cbox_id/management/generated/environment.py index 0f7ff43..4128991 100644 --- a/src/cbox_id/management/generated/environment.py +++ b/src/cbox_id/management/generated/environment.py @@ -16,6 +16,69 @@ # ── Schemas (components.schemas) ────────────────────────────────────────────────────────────────── +class ManagementKeyRequireApprovalOption1(TypedDict): + min_danger: NotRequired[Literal["read", "write", "destructive", "critical"] | None] + actions: NotRequired[list[str]] + + +class ManagementKeyCreatedByOption1(TypedDict): + type: str + id: str | None + + +class ManagementKey(TypedDict): + """A management key of this environment. `token` is present once, on the answer that minted it, and never again.""" + + id: str + name: str + description: NotRequired[str | None] + + #: The key's first characters, to recognise it by. + prefix: str + scopes: list[str] + active: bool + + #: The key that minted this one, if a key did. + parent_key_id: NotRequired[str | None] + + #: The key this one replaced, if it came from a rotation. + rotated_from_id: NotRequired[str | None] + + #: The approval policy: actions held for the person behind the key. + require_approval: NotRequired[ManagementKeyRequireApprovalOption1 | None] + created_by: NotRequired[ManagementKeyCreatedByOption1 | None] + expires_at: NotRequired[str | None] + last_used_at: NotRequired[str | None] + revoked_at: NotRequired[str | None] + created_at: str | None + + #: The key's value. Only on the answer that minted it; null on an idempotent replay of that answer, because the value is never stored. + token: NotRequired[str | None] + + +class LegacyLoginProbe(TypedDict): + #: What the declared legacy-login endpoint answered for the address, in words. + result: str + + +class ManifestSync(TypedDict): + #: The manifest matched what is already stored. + unchanged: bool + roles_declared: int + permissions_declared: int + + #: Roles stored here that the manifest no longer declares. + orphaned_role_keys: list[str] + orphaned_permission_keys: list[str] + + +class SelfServiceSignup(TypedDict): + enabled: bool + + #: False on a single-tenant install, where sign-up follows CBOX_ID_SIGNUP_MODE and the switch changes nothing. + decided_here: bool + + class Device(TypedDict): id: str install_id: str @@ -283,12 +346,20 @@ class Webhook(TypedDict): #: false while paused. active: bool + + #: How deliveries are signed. `cbox`: `X-Cbox-Timestamp` and + #: `X-Cbox-Signature: t=,v1=.">`. + #: `standard_webhooks`: `webhook-id`, `webhook-timestamp` and + #: `webhook-signature: v1,..">`, verifiable + #: with any Standard Webhooks library. Only the chosen scheme's headers are sent. + signature_scheme: Literal["cbox", "standard_webhooks"] consecutive_failures: NotRequired[int] last_success_at: NotRequired[str | None] created_at: NotRequired[str | None] #: The signing secret, on the create and rotate answers only — shown once, never - #: retrievable again. `null` on an idempotent replay of that answer. + #: retrievable again. `null` on an idempotent replay of that answer. 64 hex + #: characters under `cbox`; a `whsec_` secret under `standard_webhooks`. secret: NotRequired[str | None] @@ -306,21 +377,61 @@ class InlineHook(TypedDict): secret: NotRequired[str | None] +class LogStreamOptions(TypedDict): + """Datadog, S3 and GCS: the destination's settings, as stored. Never a credential — the API key, secret access key or service-account key is the encrypted secret. null for an HTTP collector.""" + + site: NotRequired[Literal["datadoghq.com", "us3.datadoghq.com", "us5.datadoghq.com", "datadoghq.eu", "ap1.datadoghq.com", "ap2.datadoghq.com", "ddog-gov.com"]] + service: NotRequired[str] + source: NotRequired[str] + hostname: NotRequired[str] + + #: Comma-separated `key:value` tags (`ddtags`). + tags: NotRequired[str] + bucket: NotRequired[str] + region: NotRequired[str] + prefix: NotRequired[str] + access_key_id: NotRequired[str] + role_arn: NotRequired[str] + external_id: NotRequired[str] + sse: NotRequired[Literal["AES256", "aws:kms"]] + kms_key_id: NotRequired[str] + path_style: NotRequired[bool] + gzip: NotRequired[bool] + + class LogStream(TypedDict): id: str name: str - destination: Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json"] + destination: Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json", "datadog", "s3", "gcs"] + + #: Where entries go. For Datadog, S3 and GCS, derived from the site or region unless a custom endpoint was given. endpoint_url: str + + #: An HTTP collector's scheme; `none` for the cloud destinations, which authenticate their own way. auth: Literal["none", "bearer", "splunk", "hmac"] + #: Datadog, S3 and GCS: the destination's settings, as stored. Never a credential — the API key, secret access key or service-account key is the encrypted secret. null for an HTTP collector. + options: LogStreamOptions | None + + #: An assumed-role S3 stream's external ID, generated for it: the role's trust policy must require it (`sts:ExternalId`). null otherwise. + external_id: str | None + #: null when the environment owns it: it carries EVERY organization's entries. organization_id: str | None enabled: bool + + #: `degraded`: recent transient failures, still retrying. `paused`: the circuit is open after repeated failures and resumes on its own. `action_required`: the destination refused the credential or the settings — update the stream, or run a successful test. + health: Literal["healthy", "degraded", "paused", "action_required"] consecutive_failures: NotRequired[int] last_success_at: NotRequired[str | None] + + #: The latest failure, scrubbed of the stream's secret. + last_error: NotRequired[str | None] + last_failure_kind: NotRequired[Literal["transient", "authentication", "configuration"] | None] + last_failure_at: NotRequired[str | None] created_at: NotRequired[str | None] - #: A generated HMAC key, on the create answer only — shown once. `null` on an idempotent replay. + #: A generated HMAC key, on the answer that generated it only — shown once. `null` on an idempotent replay. A credential the caller supplied is never echoed. secret: NotRequired[str | None] @@ -685,10 +796,13 @@ class SsoConnectionConfig(TypedDict): class SsoConnectionServiceProvider(TypedDict): - """What to paste into the identity provider: this connection's own entity id and ACS URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection.""" + """What to paste into the identity provider: this connection's own entity id, ACS URL and SP metadata URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection.""" sp_entity_id: NotRequired[str] sp_acs_url: NotRequired[str] + + #: SAML: this connection's service-provider metadata (entity id and ACS URL as one XML document), for an identity provider that imports SP metadata. Served for a draft too, before the identity provider's half is known. + sp_metadata_url: NotRequired[str] redirect_uri: NotRequired[str] @@ -713,7 +827,7 @@ class SsoConnection(TypedDict): #: The settings that are not secrets. config: SsoConnectionConfig - #: What to paste into the identity provider: this connection's own entity id and ACS URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection. + #: What to paste into the identity provider: this connection's own entity id, ACS URL and SP metadata URL (SAML), or its redirect URI (OIDC). null for a social sign-in connection. service_provider: NotRequired[SsoConnectionServiceProvider | None] created_at: NotRequired[str | None] updated_at: NotRequired[str | None] @@ -757,9 +871,12 @@ class SsoCertificates(TypedDict): class LogStreamTest(TypedDict): id: str - #: Whether the destination accepted the test entry. + #: Whether the destination accepted the test event (action `siem.stream.test`). delivered: bool + #: Why not: `transient` (unreachable, busy — retrying can help) or `authentication` / `configuration` (somebody has to fix the stream). null when delivered. + failure: Literal["transient", "authentication", "configuration"] | None + #: The destination's refusal, scrubbed of the stream's secret. error: str | None tested_at: str @@ -811,6 +928,40 @@ class PortalLink(TypedDict): expires_at: str | None +class PortalLinkRecord(TypedDict): + """An Admin Portal link as a list shows it. Never its URL: that was shown once, when it was minted, and only its hash is kept.""" + + id: str + organization_id: str + + #: What the link may set up. + intents: list[Literal["sso", "dsync", "domain_verification", "log_streams", "certificate_renewal", "audit_logs"]] + + #: `pending`: not opened yet, and it still can be. `in_use`: opened, and the setup + #: session it started may still be running. `completed`: its setup was finished. + #: `expired`: never opened in time, or its setup session ran out. `revoked`: withdrawn. + status: Literal["pending", "in_use", "completed", "expired", "revoked"] + created_at: str | None + + #: Who minted it: the console person's id, or the management key's. + created_by: str + + #: The address it was mailed to, or null when it was not sent. + emailed_to: str | None + + #: How long it could wait to be opened. + expires_at: str | None + + #: When it was opened. It is single-use. + consumed_at: str | None + + #: When its setup was finished. + completed_at: str | None + + #: When it was withdrawn. + revoked_at: str | None + + class AuditLogEventActor(TypedDict): id: str type: str @@ -1298,9 +1449,6 @@ class AppsManifestSetBody(TypedDict): manifest_url: NotRequired[str | None] -AppsManifestSyncData: TypeAlias = dict[str, Any] - - class AppsScopesSetBody(TypedDict): #: Required. The COMPLETE scope set afterwards; an empty list removes every scope. scopes: NotRequired[list[str]] @@ -1865,9 +2013,6 @@ class KeysCreateBody(TypedDict): require_approval: NotRequired[KeysCreateBodyRequireApproval] -KeysCreateData: TypeAlias = dict[str, Any] - - class KeysListQuery(TypedDict): #: Items per page, 1–100. Default 50. limit: NotRequired[int] @@ -1876,10 +2021,7 @@ class KeysListQuery(TypedDict): after: NotRequired[str] -KeysListItem: TypeAlias = dict[str, Any] - - -KeysListData: TypeAlias = list[dict[str, Any]] +KeysListData: TypeAlias = 'list[ManagementKey]' class KeysRotateBody(TypedDict): @@ -1887,30 +2029,74 @@ class KeysRotateBody(TypedDict): grace_hours: NotRequired[int] -KeysRotateData: TypeAlias = dict[str, Any] - - class LegacyLoginProbeBody(TypedDict): #: An address that exists in your old system — your own. email: str -LegacyLoginProbeData: TypeAlias = dict[str, Any] +class LogStreamsCreateBodyOptions(TypedDict): + """Datadog, S3 and GCS only: the destination's settings. An HTTP collector takes none.""" + + + #: Datadog: the site your account lives on — the domain you sign in at, e.g. `datadoghq.eu` for EU1. Default `datadoghq.com` (US1). An API key only works on its own site. + site: NotRequired[Literal["datadoghq.com", "us3.datadoghq.com", "us5.datadoghq.com", "datadoghq.eu", "ap1.datadoghq.com", "ap2.datadoghq.com", "ddog-gov.com"] | None] + + #: Datadog: the `service` attribute on every entry. Default: this platform's name. + service: NotRequired[str | None] + + #: Datadog: the `ddsource` attribute. Default `cbox`. + source: NotRequired[str | None] + + #: Datadog: `key:value` tags (`ddtags`) on every entry, e.g. `env:prod`. + tags: NotRequired[list[str] | None] + + #: Datadog: the `hostname` attribute. Default: this platform's host. + hostname: NotRequired[str | None] + + #: S3 and GCS: the bucket objects are written to. + bucket: NotRequired[str | None] + + #: S3: the bucket's AWS Region, e.g. `eu-west-1` (`auto` for Cloudflare R2). + region: NotRequired[str | None] + + #: S3 and GCS: the object key prefix, e.g. `cbox/audit`. Objects are written to `{prefix}/{yyyy}/{mm}/{dd}/{hh}/{batch}.ndjson.gz`. + prefix: NotRequired[str | None] + + #: S3 with an access key: the IAM access key ID. Its secret access key is the stream's `secret`. + access_key_id: NotRequired[str | None] + + #: S3 with an assumed role: the IAM role the platform assumes. No secret is stored; the role's trust policy must require the stream's `external_id`. + role_arn: NotRequired[str | None] + + #: S3: server-side encryption requested on every object. Left out, the bucket's default. + sse: NotRequired[Literal["AES256", "aws:kms"] | None] + + #: S3 with `aws:kms`: the KMS key ID, alias or ARN. + kms_key_id: NotRequired[str | None] + + #: S3: path-style addressing. Default: virtual-hosted on AWS, path-style on a custom endpoint (MinIO, R2). + path_style: NotRequired[bool | None] + + #: S3 and GCS: gzip each object (`.ndjson.gz`). Default true. + gzip: NotRequired[bool | None] class LogStreamsCreateBody(TypedDict): name: str - destination: Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json"] + destination: Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json", "datadog", "s3", "gcs"] - #: A public URL the entries are POSTed to. - endpoint_url: str + #: An HTTP collector: the public URL entries are POSTed to (required). Datadog, S3 and GCS: leave out for the destination's own endpoint, or an https URL of an S3-compatible store (MinIO, R2). + endpoint_url: NotRequired[str | None] - #: How the endpoint is authenticated. Left out, the destination's default. + #: An HTTP collector: how the endpoint is authenticated. Left out, the destination's default. The cloud destinations authenticate their own way. auth: NotRequired[Literal["none", "bearer", "splunk", "hmac"]] - #: The bearer or Splunk token the endpoint expects. Left out with `hmac`, a key is generated and returned once. + #: The credential, never echoed: the bearer or Splunk token (HTTP collectors; left out with `hmac`, a key is generated and returned once), the Datadog API key, the S3 secret access key (none with `role_arn`), or the GCS service-account JSON key. secret: NotRequired[str | None] + #: Datadog, S3 and GCS only: the destination's settings. An HTTP collector takes none. + options: NotRequired[LogStreamsCreateBodyOptions] + #: The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide. organization_id: NotRequired[str | None] @@ -1929,9 +2115,73 @@ class LogStreamsListQuery(TypedDict): LogStreamsListData: TypeAlias = 'list[LogStream]' +class LogStreamsUpdateBodyOptions(TypedDict): + """Datadog, S3 and GCS only: the destination's settings. An HTTP collector takes none.""" + + + #: Datadog: the site your account lives on — the domain you sign in at, e.g. `datadoghq.eu` for EU1. Default `datadoghq.com` (US1). An API key only works on its own site. + site: NotRequired[Literal["datadoghq.com", "us3.datadoghq.com", "us5.datadoghq.com", "datadoghq.eu", "ap1.datadoghq.com", "ap2.datadoghq.com", "ddog-gov.com"] | None] + + #: Datadog: the `service` attribute on every entry. Default: this platform's name. + service: NotRequired[str | None] + + #: Datadog: the `ddsource` attribute. Default `cbox`. + source: NotRequired[str | None] + + #: Datadog: `key:value` tags (`ddtags`) on every entry, e.g. `env:prod`. + tags: NotRequired[list[str] | None] + + #: Datadog: the `hostname` attribute. Default: this platform's host. + hostname: NotRequired[str | None] + + #: S3 and GCS: the bucket objects are written to. + bucket: NotRequired[str | None] + + #: S3: the bucket's AWS Region, e.g. `eu-west-1` (`auto` for Cloudflare R2). + region: NotRequired[str | None] + + #: S3 and GCS: the object key prefix, e.g. `cbox/audit`. Objects are written to `{prefix}/{yyyy}/{mm}/{dd}/{hh}/{batch}.ndjson.gz`. + prefix: NotRequired[str | None] + + #: S3 with an access key: the IAM access key ID. Its secret access key is the stream's `secret`. + access_key_id: NotRequired[str | None] + + #: S3 with an assumed role: the IAM role the platform assumes. No secret is stored; the role's trust policy must require the stream's `external_id`. + role_arn: NotRequired[str | None] + + #: S3: server-side encryption requested on every object. Left out, the bucket's default. + sse: NotRequired[Literal["AES256", "aws:kms"] | None] + + #: S3 with `aws:kms`: the KMS key ID, alias or ARN. + kms_key_id: NotRequired[str | None] + + #: S3: path-style addressing. Default: virtual-hosted on AWS, path-style on a custom endpoint (MinIO, R2). + path_style: NotRequired[bool | None] + + #: S3 and GCS: gzip each object (`.ndjson.gz`). Default true. + gzip: NotRequired[bool | None] + + class LogStreamsUpdateBody(TypedDict): + name: NotRequired[str] + + #: Changing it starts the stream's options and credential afresh. + destination: NotRequired[Literal["splunk_hec", "elastic_ecs", "graylog_gelf", "cef_http", "generic_json", "datadog", "s3", "gcs"]] + + #: An HTTP collector's URL; for Datadog, S3 and GCS empty means the destination's own endpoint. + endpoint_url: NotRequired[str | None] + + #: An HTTP collector only. + auth: NotRequired[Literal["none", "bearer", "splunk", "hmac"]] + + #: A new credential (token, API key, secret access key or service-account JSON key). Left out, the current one is kept. Never echoed. + secret: NotRequired[str | None] + + #: Datadog, S3 and GCS only: the destination's settings. An HTTP collector takes none. + options: NotRequired[LogStreamsUpdateBodyOptions] + #: True to deliver; false to stop, keeping what is pending. - enabled: bool + enabled: NotRequired[bool] class MembersAddBody(TypedDict): @@ -2037,6 +2287,9 @@ class OrganizationsPortalLinksCreateBody(TypedDict): locale: NotRequired[Literal["en", "da", "de", "sv", "nb", "fr"] | None] +OrganizationsPortalLinksListData: TypeAlias = 'list[PortalLinkRecord]' + + class OrganizationsTransferOwnershipBody(TypedDict): user_id: str @@ -2298,9 +2551,6 @@ class SigninSelfServiceSignupSetBody(TypedDict): enabled: bool -SigninSelfServiceSignupSetData: TypeAlias = dict[str, Any] - - class SigninSocialDeleteBody(TypedDict): #: Only remove it if it is this organization's. organization_id: NotRequired[str | None] @@ -2350,7 +2600,7 @@ class SigninSocialSetBody(TypedDict): #: The organization whose sign-in page offers it. organization_id: str - #: The catalogue key: google, microsoft, okta, auth0, keycloak, gitlab, slack, github, discord, apple, facebook. + #: The catalogue key: google, microsoft, okta, auth0, keycloak, gitlab, slack, github, discord, apple, facebook, linkedin, bitbucket, xero, intuit. provider: str #: The client id from your own account with the provider. For Apple, the Services ID. @@ -2481,7 +2731,7 @@ class SsoConnectionsCreateBody(TypedDict): #: OIDC: the client secret. Write-only. client_secret: NotRequired[str | None] - #: OIDC: the signing key. Write-only. + #: OIDC, optional: the provider's ID-token signing key (an RS256 public key, PEM). Leave it out for any provider whose discovery document publishes a jwks_uri — the keys are read from there and follow its rotations. Needed only when it does not. Write-only. signing_key: NotRequired[str | None] @@ -2550,7 +2800,7 @@ class SsoConnectionsUpdateBody(TypedDict): #: OIDC: the client secret. Write-only. client_secret: NotRequired[str | None] - #: OIDC: the signing key. Write-only. + #: OIDC, optional: the provider's ID-token signing key (an RS256 public key, PEM). Leave it out for any provider whose discovery document publishes a jwks_uri — the keys are read from there and follow its rotations. Needed only when it does not. Write-only. signing_key: NotRequired[str | None] @@ -2761,6 +3011,9 @@ class WebhooksCreateBody(TypedDict): #: The events it receives. event_types: list[Literal["user.created", "user.updated", "user.deactivated", "user.login", "user.reactivated", "identity.linked", "user.erased", "organization.created", "organization.suspended", "organization.reactivated", "organization.updated", "organization.deleted", "membership.created", "membership.updated", "membership.deleted", "invitation.created", "invitation.accepted", "invitation.revoked", "role.assigned", "role.unassigned", "role.assigned_everywhere", "role.unassigned_everywhere", "api_key.created", "api_key.revoked", "support_session.started", "directory.user.provisioned", "directory.user.deprovisioned", "directory.user.deactivated", "directory.group.membership_changed", "domain.added", "domain.removed", "domain.verified", "connection.activated", "connection.certificate_expiring", "entitlement.set", "entitlement.updated", "entitlement.revoked", "vault.grant.created", "vault.grant.revoked", "vault.secret.revoked", "governance.access.revoked"]] + #: How deliveries are signed: `cbox` (X-Cbox-Signature, the default) or `standard_webhooks` (webhook-id / webhook-timestamp / webhook-signature, verifiable with any Standard Webhooks library; the secret is a `whsec_` secret). + signature_scheme: NotRequired[Literal["cbox", "standard_webhooks"]] + #: The organization it belongs to; it carries that organization's traffic only. Send this or environment_wide. organization_id: NotRequired[str | None] @@ -2779,6 +3032,11 @@ class WebhooksListQuery(TypedDict): WebhooksListData: TypeAlias = 'list[Webhook]' +class WebhooksSignatureSchemeChangeBody(TypedDict): + #: The scheme deliveries are signed with from the next attempt. The secret is unchanged: under `standard_webhooks` a 64-hex secret is used as `whsec_` + base64 of the hex string. Update the receiver before you switch. + signature_scheme: Literal["cbox", "standard_webhooks"] + + class WebhooksUpdateBody(TypedDict): #: A public HTTPS URL. Left out, unchanged. url: NotRequired[str] @@ -2801,25 +3059,25 @@ class WebhooksUpdateBody(TypedDict): "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), "api_keys.list": OperationSpec(action="api_keys.list", operation_id="api_keys_list", method="GET", path="/organizations/{organization_id}/api-keys", path_params=("organization_id",), scope="api_keys:read", danger=None, approval=True, body=False, pagination="cursor"), "api_keys.revoke": OperationSpec(action="api_keys.revoke", operation_id="api_keys_revoke", method="DELETE", path="/api-keys/{id}", path_params=("id",), scope="api_keys:write", danger=None, approval=True, body=False, pagination=None), - "apis.create": OperationSpec(action=None, operation_id=None, method="POST", path="/apis", path_params=(), scope="apis:write", danger=None, approval=False, body=True, pagination=None), - "apis.delete": OperationSpec(action=None, operation_id=None, method="DELETE", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=False, body=False, pagination=None), - "apis.get": OperationSpec(action=None, operation_id=None, method="GET", path="/apis/{id}", path_params=("id",), scope="apis:read", danger=None, approval=False, body=False, pagination=None), - "apis.list": OperationSpec(action=None, operation_id=None, method="GET", path="/apis", path_params=(), scope="apis:read", danger=None, approval=False, body=False, pagination="cursor"), + "apis.create": OperationSpec(action=None, operation_id=None, method="POST", path="/apis", path_params=(), scope="apis:write", danger=None, approval=True, body=True, pagination=None), + "apis.delete": OperationSpec(action=None, operation_id=None, method="DELETE", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=True, body=False, pagination=None), + "apis.get": OperationSpec(action=None, operation_id=None, method="GET", path="/apis/{id}", path_params=("id",), scope="apis:read", danger=None, approval=True, body=False, pagination=None), + "apis.list": OperationSpec(action=None, operation_id=None, method="GET", path="/apis", path_params=(), scope="apis:read", danger=None, approval=True, body=False, pagination="cursor"), "apis.scopes.define": OperationSpec(action="apis.scopes.define", operation_id="apis_scopes_define", method="PUT", path="/apis/{id}/scopes/{key}", path_params=("id", "key"), scope="apis:write", danger="write", approval=True, body=True, pagination=None), "apis.scopes.remove": OperationSpec(action="apis.scopes.remove", operation_id="apis_scopes_remove", method="DELETE", path="/apis/{id}/scopes/{key}", path_params=("id", "key"), scope="apis:write", danger="destructive", approval=True, body=False, pagination=None), - "apis.update": OperationSpec(action=None, operation_id=None, method="PATCH", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=False, body=True, pagination=None), + "apis.update": OperationSpec(action=None, operation_id=None, method="PATCH", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=True, body=True, pagination=None), "approvals.deny": OperationSpec(action="approvals.deny", operation_id="approvals_deny", method="POST", path="/agent-requests/{request_id}/deny", path_params=("request_id",), scope="approvals:write", danger="destructive", approval=True, body=False, pagination=None), "approvals.list": OperationSpec(action="approvals.list", operation_id="approvals_list", method="GET", path="/agent-requests", path_params=(), scope="approvals:read", danger="read", approval=True, body=False, pagination="cursor"), - "apps.blueprint": OperationSpec(action="apps.blueprint", operation_id="apps_blueprint", method="GET", path="/apps/{id}/blueprint", path_params=("id",), scope="apps:read", danger=None, approval=False, body=False, pagination=None), + "apps.blueprint": OperationSpec(action="apps.blueprint", operation_id="apps_blueprint", method="GET", path="/apps/{id}/blueprint", path_params=("id",), scope="apps:read", danger=None, approval=True, body=False, pagination=None), "apps.copy": OperationSpec(action="apps.copy", operation_id="apps_copy", method="POST", path="/apps/{id}/copy", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), - "apps.create": OperationSpec(action="apps.create", operation_id="apps_create", method="POST", path="/apps", path_params=(), scope="apps:write", danger="critical", approval=False, body=True, pagination=None), + "apps.create": OperationSpec(action="apps.create", operation_id="apps_create", method="POST", path="/apps", path_params=(), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), "apps.delete": OperationSpec(action="apps.delete", operation_id="apps_delete", method="DELETE", path="/apps/{id}", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=False, pagination=None), "apps.get": OperationSpec(action="apps.get", operation_id="apps_get", method="GET", path="/apps/{id}", path_params=("id",), scope="apps:read", danger="read", approval=True, body=False, pagination=None), - "apps.list": OperationSpec(action="apps.list", operation_id="apps_list", method="GET", path="/apps", path_params=(), scope="apps:read", danger=None, approval=False, body=False, pagination="cursor"), + "apps.list": OperationSpec(action="apps.list", operation_id="apps_list", method="GET", path="/apps", path_params=(), scope="apps:read", danger=None, approval=True, body=False, pagination="cursor"), "apps.manifest.set": OperationSpec(action="apps.manifest.set", operation_id="apps_manifest_set", method="PUT", path="/apps/{id}/manifest", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), "apps.manifest.sync": OperationSpec(action="apps.manifest.sync", operation_id="apps_manifest_sync", method="POST", path="/apps/{id}/manifest/sync", path_params=("id",), scope="apps:write", danger="write", approval=True, body=False, pagination=None), "apps.scopes.set": OperationSpec(action="apps.scopes.set", operation_id="apps_scopes_set", method="PUT", path="/apps/{id}/scopes", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), - "apps.secrets.list": OperationSpec(action="apps.secrets.list", operation_id="apps_secrets_list", method="GET", path="/apps/{id}/secrets", path_params=("id",), scope="apps:read", danger=None, approval=False, body=False, pagination=None), + "apps.secrets.list": OperationSpec(action="apps.secrets.list", operation_id="apps_secrets_list", method="GET", path="/apps/{id}/secrets", path_params=("id",), scope="apps:read", danger=None, approval=True, body=False, pagination=None), "apps.secrets.revoke": OperationSpec(action="apps.secrets.revoke", operation_id="apps_secrets_revoke", method="DELETE", path="/apps/{id}/secrets/{secret_id}", path_params=("id", "secret_id"), scope="apps:write", danger="critical", approval=True, body=False, pagination=None), "apps.secrets.rotate": OperationSpec(action="apps.secrets.rotate", operation_id="apps_secrets_rotate", method="POST", path="/apps/{id}/secrets", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), "apps.settings.api_key_prefix": OperationSpec(action="apps.settings.api_key_prefix", operation_id="apps_settings_api_key_prefix", method="PUT", path="/apps/{id}/settings/api-key-prefix", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), @@ -2903,6 +3161,8 @@ class WebhooksUpdateBody(TypedDict): "organizations.get": OperationSpec(action="organizations.get", operation_id="organizations_get", method="GET", path="/organizations/{id}", path_params=("id",), scope="organizations:read", danger=None, approval=True, body=False, pagination=None), "organizations.list": OperationSpec(action="organizations.list", operation_id="organizations_list", method="GET", path="/organizations", path_params=(), scope="organizations:read", danger=None, approval=True, body=False, pagination="cursor"), "organizations.portal_links.create": OperationSpec(action="organizations.portal_links.create", operation_id="organizations_portal_links_create", method="POST", path="/organizations/{organization_id}/portal-links", path_params=("organization_id",), scope="portal_links:write", danger="critical", approval=True, body=True, pagination=None), + "organizations.portal_links.list": OperationSpec(action="organizations.portal_links.list", operation_id="organizations_portal_links_list", method="GET", path="/organizations/{organization_id}/portal-links", path_params=("organization_id",), scope="portal_links:read", danger="read", approval=True, body=False, pagination=None), + "organizations.portal_links.revoke": OperationSpec(action="organizations.portal_links.revoke", operation_id="organizations_portal_links_revoke", method="DELETE", path="/organizations/{organization_id}/portal-links/{id}", path_params=("organization_id", "id"), scope="portal_links:write", danger="destructive", approval=True, body=False, pagination=None), "organizations.reactivate": OperationSpec(action="organizations.reactivate", operation_id="organizations_reactivate", method="POST", path="/organizations/{id}/reactivate", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), "organizations.suspend": OperationSpec(action="organizations.suspend", operation_id="organizations_suspend", method="POST", path="/organizations/{id}/suspend", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), "organizations.transfer_ownership": OperationSpec(action="organizations.transfer_ownership", operation_id="organizations_transfer_ownership", method="POST", path="/organizations/{id}/transfer-ownership", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), @@ -2992,6 +3252,7 @@ class WebhooksUpdateBody(TypedDict): "webhooks.pause": OperationSpec(action="webhooks.pause", operation_id="webhooks_pause", method="POST", path="/webhooks/{id}/pause", path_params=("id",), scope="webhooks:write", danger="write", approval=True, body=False, pagination=None), "webhooks.resume": OperationSpec(action="webhooks.resume", operation_id="webhooks_resume", method="POST", path="/webhooks/{id}/resume", path_params=("id",), scope="webhooks:write", danger="write", approval=True, body=False, pagination=None), "webhooks.secret.rotate": OperationSpec(action="webhooks.secret.rotate", operation_id="webhooks_secret_rotate", method="POST", path="/webhooks/{id}/rotate", path_params=("id",), scope="webhooks:write", danger="critical", approval=True, body=False, pagination=None), + "webhooks.signature_scheme.change": OperationSpec(action="webhooks.signature_scheme.change", operation_id="webhooks_signature_scheme_change", method="POST", path="/webhooks/{id}/signature-scheme", path_params=("id",), scope="webhooks:write", danger="destructive", approval=True, body=True, pagination=None), "webhooks.update": OperationSpec(action="webhooks.update", operation_id="webhooks_update", method="PATCH", path="/webhooks/{id}", path_params=("id",), scope="webhooks:write", danger="write", approval=True, body=True, pagination=None), } @@ -3237,7 +3498,13 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core self.scopes = ApisScopesMethods(core) - def create(self, body: ApisCreateBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: + @overload + def create(self, body: ApisCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: ... + + @overload + def create(self, body: ApisCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: ... + + def create(self, body: ApisCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: """Register an API and its scopes Requires scope `apis:write`. Registering an API is the environment's act; no tenant @@ -3249,10 +3516,17 @@ def create(self, body: ApisCreateBody, *, idempotency_key: str | None = None, he ``POST /apis`` Scope ``apis:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apis.create"], (), body, idempotency_key=idempotency_key, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apis.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def delete(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... - def delete(self, id: str, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: + def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: """Delete an API and its scopes Requires scope `apis:write`. Tokens already minted for it keep their `aud` until they @@ -3260,34 +3534,55 @@ def delete(self, id: str, *, idempotency_key: str | None = None, headers: Mappin ``DELETE /apis/{id}`` Scope ``apis:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apis.delete"], (id,), None, idempotency_key=idempotency_key, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apis.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def get(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: ... + + @overload + def get(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: ... - def get(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: + def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: """Get an API Requires scope `apis:read`. ``GET /apis/{id}`` Scope ``apis:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apis.get"], (id,), None, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apis.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: ApisListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApisListData]: ... - def list(self, query: ApisListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ApisListData]: + @overload + def list(self, query: ApisListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApisListData] | PendingApprovalResult[ApisListData]: ... + + def list(self, query: ApisListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ApisListData] | PendingApprovalResult[ApisListData]: """List registered APIs Requires scope `apis:read`. ``GET /apis`` Scope ``apis:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apis.list"], (), query, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apis.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) def list_all(self, query: ApisListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Api]: """Every item of ``apis.list``, fetching pages as the iteration reaches them.""" return self._core.paginate(ENVIRONMENT_OPERATIONS["apis.list"], (), query, headers=headers) - def update(self, id: str, body: ApisUpdateBody | None = None, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: + @overload + def update(self, id: str, body: ApisUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api]: ... + + @overload + def update(self, id: str, body: ApisUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: ... + + def update(self, id: str, body: ApisUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Api] | PendingApprovalResult[Api]: """Change an API Requires scope `apis:write`. Any of: `name`; `client_id` (null unlinks the app); @@ -3297,8 +3592,9 @@ def update(self, id: str, body: ApisUpdateBody | None = None, *, idempotency_key ``PATCH /apis/{id}`` Scope ``apis:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apis.update"], (id,), body, idempotency_key=idempotency_key, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apis.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) class ApprovalsMethods: @@ -3370,12 +3666,12 @@ def set(self, id: str, body: AppsManifestSetBody | None = None, *, approval: App return self._core.call(ENVIRONMENT_OPERATIONS["apps.manifest.set"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @overload - def sync(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsManifestSyncData]: ... + def sync(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManifestSync]: ... @overload - def sync(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsManifestSyncData] | PendingApprovalResult[AppsManifestSyncData]: ... + def sync(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManifestSync] | PendingApprovalResult[ManifestSync]: ... - def sync(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsManifestSyncData] | PendingApprovalResult[AppsManifestSyncData]: + def sync(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManifestSync] | PendingApprovalResult[ManifestSync]: """Fetch an app's published manifest now and sync the roles and permissions it declares. Requires scope `apps:write`. Danger: write. @@ -3417,7 +3713,13 @@ class AppsSecretsMethods: def __init__(self, core: ManagementTransport) -> None: self._core = core - def list(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsSecretsListData]: + @overload + def list(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsSecretsListData]: ... + + @overload + def list(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsSecretsListData] | PendingApprovalResult[AppsSecretsListData]: ... + + def list(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsSecretsListData] | PendingApprovalResult[AppsSecretsListData]: """List an app's live client secrets Requires scope `apps:read`. Every live secret, newest first: its id (what @@ -3427,8 +3729,9 @@ def list(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiRespo ``GET /apps/{id}/secrets`` · action ``apps.secrets.list`` Scope ``apps:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apps.secrets.list"], (id,), None, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apps.secrets.list"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @overload def revoke(self, id: str, secret_id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... @@ -3550,7 +3853,13 @@ def __init__(self, core: ManagementTransport) -> None: self.secrets = AppsSecretsMethods(core) self.settings = AppsSettingsMethods(core) - def blueprint(self, id: str, *, headers: Mapping[str, str] | None = None) -> ApiResponse[AppBlueprint]: + @overload + def blueprint(self, id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppBlueprint]: ... + + @overload + def blueprint(self, id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppBlueprint] | PendingApprovalResult[AppBlueprint]: ... + + def blueprint(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppBlueprint] | PendingApprovalResult[AppBlueprint]: """Export an app's configuration as a blueprint Requires scope `apps:read`. `{id}` is the app's id or its `client_id`. The document @@ -3560,8 +3869,9 @@ def blueprint(self, id: str, *, headers: Mapping[str, str] | None = None) -> Api ``GET /apps/{id}/blueprint`` · action ``apps.blueprint`` Scope ``apps:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apps.blueprint"], (id,), None, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apps.blueprint"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @overload def copy(self, id: str, body: AppsCopyBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... @@ -3580,7 +3890,13 @@ def copy(self, id: str, body: AppsCopyBody, *, approval: ApprovalMode = "wait", """ return self._core.call(ENVIRONMENT_OPERATIONS["apps.copy"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) - def create(self, body: AppsCreateBody | None = None, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: + @overload + def create(self, body: AppsCreateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App]: ... + + @overload + def create(self, body: AppsCreateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: ... + + def create(self, body: AppsCreateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[App] | PendingApprovalResult[App]: """Register an app Requires scope `apps:write`. Two ways to describe it: @@ -3603,8 +3919,9 @@ def create(self, body: AppsCreateBody | None = None, *, idempotency_key: str | N ``POST /apps`` · action ``apps.create`` Scope ``apps:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apps.create"], (), body, idempotency_key=idempotency_key, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apps.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @overload def delete(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... @@ -3640,15 +3957,22 @@ def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | No """ return self._core.call(ENVIRONMENT_OPERATIONS["apps.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) - def list(self, query: AppsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsListData]: + @overload + def list(self, query: AppsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsListData]: ... + + @overload + def list(self, query: AppsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsListData] | PendingApprovalResult[AppsListData]: ... + + def list(self, query: AppsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[AppsListData] | PendingApprovalResult[AppsListData]: """List apps Requires scope `apps:read`. Never a secret. ``GET /apps`` · action ``apps.list`` Scope ``apps:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(ENVIRONMENT_OPERATIONS["apps.list"], (), query, headers=headers) + return self._core.call(ENVIRONMENT_OPERATIONS["apps.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) def list_all(self, query: AppsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[App]: """Every item of ``apps.list``, fetching pages as the iteration reaches them.""" @@ -4640,12 +4964,12 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core @overload - def create(self, body: KeysCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysCreateData]: ... + def create(self, body: KeysCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManagementKey]: ... @overload - def create(self, body: KeysCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysCreateData] | PendingApprovalResult[KeysCreateData]: ... + def create(self, body: KeysCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManagementKey] | PendingApprovalResult[ManagementKey]: ... - def create(self, body: KeysCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysCreateData] | PendingApprovalResult[KeysCreateData]: + def create(self, body: KeysCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManagementKey] | PendingApprovalResult[ManagementKey]: """Mint a management key for this environment, at most as wide as the caller. The value is returned once, as `token`. Requires scope `keys:write`. Danger: critical. @@ -4673,7 +4997,7 @@ def list(self, query: KeysListQuery | None = None, *, approval: ApprovalMode = " """ return self._core.call(ENVIRONMENT_OPERATIONS["keys.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) - def list_all(self, query: KeysListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[KeysListItem]: + def list_all(self, query: KeysListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[ManagementKey]: """Every item of ``keys.list``, fetching pages as the iteration reaches them.""" return self._core.paginate(ENVIRONMENT_OPERATIONS["keys.list"], (), query, headers=headers) @@ -4695,12 +5019,12 @@ def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s return self._core.call(ENVIRONMENT_OPERATIONS["keys.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @overload - def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysRotateData]: ... + def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManagementKey]: ... @overload - def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysRotateData] | PendingApprovalResult[KeysRotateData]: ... + def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManagementKey] | PendingApprovalResult[ManagementKey]: ... - def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[KeysRotateData] | PendingApprovalResult[KeysRotateData]: + def rotate(self, id: str, body: KeysRotateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ManagementKey] | PendingApprovalResult[ManagementKey]: """Rotate a management key: mint a successor with the same scopes and retire the old one after a grace period. Requires scope `keys:write`. Danger: critical. @@ -4753,12 +5077,12 @@ def get(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None return self._core.call(ENVIRONMENT_OPERATIONS["legacy_login.get"], (), None, approval=approval, approval_id=approval_id, headers=headers) @overload - def probe(self, body: LegacyLoginProbeBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbeData]: ... + def probe(self, body: LegacyLoginProbeBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbe]: ... @overload - def probe(self, body: LegacyLoginProbeBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbeData] | PendingApprovalResult[LegacyLoginProbeData]: ... + def probe(self, body: LegacyLoginProbeBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbe] | PendingApprovalResult[LegacyLoginProbe]: ... - def probe(self, body: LegacyLoginProbeBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbeData] | PendingApprovalResult[LegacyLoginProbeData]: + def probe(self, body: LegacyLoginProbeBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LegacyLoginProbe] | PendingApprovalResult[LegacyLoginProbe]: """Ask the declared legacy login endpoint whether it knows an address — your own — to test it before approving. Sends no password. Requires scope `signin:write`. Danger: write. @@ -4800,7 +5124,7 @@ def create(self, body: LogStreamsCreateBody, *, approval: Literal["wait"] = "wai def create(self, body: LogStreamsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: ... def create(self, body: LogStreamsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: - """Stream the audit trail to a SIEM (Splunk, Elastic, Graylog, CEF, JSON). A generated HMAC key is returned once. + """Stream the audit trail to a SIEM (Splunk, Elastic, Graylog, CEF, JSON), Datadog, or an S3 or GCS bucket. A generated HMAC key is returned once. Requires scope `log_streams:write`. Danger: critical. @@ -4872,7 +5196,7 @@ def test(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: def test(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamTest] | PendingApprovalResult[LogStreamTest]: ... def test(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStreamTest] | PendingApprovalResult[LogStreamTest]: - """Send one test entry to a log stream now and report whether the SIEM accepted it. + """Send one test event to a log stream now and report whether the destination accepted it, and if not, why. Requires scope `log_streams:write`. Danger: write. @@ -4883,13 +5207,13 @@ def test(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str return self._core.call(ENVIRONMENT_OPERATIONS["log_streams.test"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @overload - def update(self, id: str, body: LogStreamsUpdateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream]: ... + def update(self, id: str, body: LogStreamsUpdateBody | None = None, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream]: ... @overload - def update(self, id: str, body: LogStreamsUpdateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: ... + def update(self, id: str, body: LogStreamsUpdateBody | None = None, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: ... - def update(self, id: str, body: LogStreamsUpdateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: - """Disable (enabled: false) or resume (enabled: true) an audit log stream. Disabled, entries are kept and delivered on resume. + def update(self, id: str, body: LogStreamsUpdateBody | None = None, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream]: + """Change an audit log stream's name, destination, endpoint, options or credential (re-validated; resets its circuit breaker), or disable (enabled: false) / resume (enabled: true) it. Requires scope `log_streams:write`. Danger: critical. @@ -5182,6 +5506,44 @@ def create(self, organization_id: str, body: OrganizationsPortalLinksCreateBody, """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.portal_links.create"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + @overload + def list(self, organization_id: str, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsPortalLinksListData]: ... + + @overload + def list(self, organization_id: str, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsPortalLinksListData] | PendingApprovalResult[OrganizationsPortalLinksListData]: ... + + def list(self, organization_id: str, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[OrganizationsPortalLinksListData] | PendingApprovalResult[OrganizationsPortalLinksListData]: + """List an organization's Admin Portal links + + Requires scope `portal_links:read`. Danger: read. The links minted in the last 30 + days, newest first — every link that can still be opened is among them, since a link + waits a week at most. Each says what it opens, who minted it, whom it was mailed to + and where it stands (`status`). Never the link itself: it was shown once, when it was + minted, and only its hash is kept. Not paged. + + ``GET /organizations/{organization_id}/portal-links`` · action ``organizations.portal_links.list`` + Scope ``portal_links:read`` · danger: read. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.portal_links.list"], (organization_id,), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def revoke(self, organization_id: str, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, organization_id: str, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, organization_id: str, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: + """Withdraw an Admin Portal link: it can no longer be opened, and a setup session it already opened ends on its next request. What was already set up through it stays. + + Requires scope `portal_links:write`. Danger: destructive. + + ``DELETE /organizations/{organization_id}/portal-links/{id}`` · action ``organizations.portal_links.revoke`` + Scope ``portal_links:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["organizations.portal_links.revoke"], (organization_id, id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + class OrganizationsMethods: """``organizations.*``""" @@ -5836,12 +6198,12 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core @overload - def set(self, body: SigninSelfServiceSignupSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSelfServiceSignupSetData]: ... + def set(self, body: SigninSelfServiceSignupSetBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SelfServiceSignup]: ... @overload - def set(self, body: SigninSelfServiceSignupSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSelfServiceSignupSetData] | PendingApprovalResult[SigninSelfServiceSignupSetData]: ... + def set(self, body: SigninSelfServiceSignupSetBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SelfServiceSignup] | PendingApprovalResult[SelfServiceSignup]: ... - def set(self, body: SigninSelfServiceSignupSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SigninSelfServiceSignupSetData] | PendingApprovalResult[SigninSelfServiceSignupSetData]: + def set(self, body: SigninSelfServiceSignupSetBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[SelfServiceSignup] | PendingApprovalResult[SelfServiceSignup]: """Switch self-service sign-up on or off: whether people can create an account and their own organization in this environment. Requires scope `signin:write`. Danger: critical. @@ -6992,12 +7354,37 @@ def rotate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.secret.rotate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) +class WebhooksSignatureSchemeMethods: + """``webhooks.signature_scheme.*``""" + + def __init__(self, core: ManagementTransport) -> None: + self._core = core + + @overload + def change(self, id: str, body: WebhooksSignatureSchemeChangeBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... + + @overload + def change(self, id: str, body: WebhooksSignatureSchemeChangeBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: ... + + def change(self, id: str, body: WebhooksSignatureSchemeChangeBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook]: + """Change how a webhook endpoint's deliveries are signed (cbox or standard_webhooks). No new secret is issued: a hex secret is used as whsec_ + base64 of itself. Update the receiver first. + + Requires scope `webhooks:write`. Danger: destructive. + + ``POST /webhooks/{id}/signature-scheme`` · action ``webhooks.signature_scheme.change`` + Scope ``webhooks:write`` · danger: destructive. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. + """ + return self._core.call(ENVIRONMENT_OPERATIONS["webhooks.signature_scheme.change"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + class WebhooksMethods: """``webhooks.*``""" def __init__(self, core: ManagementTransport) -> None: self._core = core self.secret = WebhooksSecretMethods(core) + self.signature_scheme = WebhooksSignatureSchemeMethods(core) @overload def create(self, body: WebhooksCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Webhook]: ... diff --git a/src/cbox_id/management/generated/workspace.py b/src/cbox_id/management/generated/workspace.py index 8acbc4b..a80b055 100644 --- a/src/cbox_id/management/generated/workspace.py +++ b/src/cbox_id/management/generated/workspace.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Iterator, Mapping -from typing import Any, Literal, TypeAlias, overload +from typing import Literal, TypeAlias, overload from typing_extensions import NotRequired, TypedDict @@ -16,12 +16,17 @@ # ── Schemas (components.schemas) ────────────────────────────────────────────────────────────────── +class VerificationResend(TypedDict): + outcome: Literal["sent", "already_provisioned"] + message: str + + class Organization(TypedDict): id: NotRequired[str] name: NotRequired[str] status: NotRequired[Literal["active", "suspended"]] - #: The organization's projects with each one's plan/allowance. Present only for billing-readers (plans are per project). + #: The workspace's projects with each one's plan/allowance. Present only for billing-readers (plans are per project). projects: NotRequired[list[Project]] @@ -181,7 +186,7 @@ class EnvironmentsCreateBody(TypedDict): name: str type: NotRequired[Literal["production", "sandbox"]] - #: The project (billing anchor) to create the environment under. Defaults to the organization's first project when omitted. + #: The project (billing anchor) to create the environment under. Defaults to the workspace's first project when omitted. project_id: NotRequired[str] #: Mint the environment's first management key in the same call. @@ -247,9 +252,6 @@ class ProjectsRenameBody(TypedDict): name: str -ProjectsVerificationResendData: TypeAlias = dict[str, Any] - - class TeamEnvironmentAccessBody(TypedDict): #: True: every environment, including ones created later. all_environments: bool @@ -289,32 +291,32 @@ class WorkspaceSettingsUpdateBody(TypedDict): #: Every operation of the Cbox ID — Workspace Management API, keyed by action name. WORKSPACE_OPERATIONS: Mapping[str, OperationSpec] = { "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/workspace/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), - "environments.create": OperationSpec(action="environments.create", operation_id="environments_create", method="POST", path="/workspace/environments", path_params=(), scope="environments:write", danger="critical", approval=False, body=True, pagination=None), + "environments.create": OperationSpec(action="environments.create", operation_id="environments_create", method="POST", path="/workspace/environments", path_params=(), scope="environments:write", danger="critical", approval=True, body=True, pagination=None), "environments.domain.remove": OperationSpec(action="environments.domain.remove", operation_id="environments_domain_remove", method="DELETE", path="/workspace/environments/{environment_id}/domain", path_params=("environment_id",), scope="environments:write", danger="destructive", approval=True, body=False, pagination=None), "environments.domain.request": OperationSpec(action="environments.domain.request", operation_id="environments_domain_request", method="POST", path="/workspace/environments/{environment_id}/domain", path_params=("environment_id",), scope="environments:write", danger="write", approval=True, body=True, pagination=None), "environments.domain.verify": OperationSpec(action="environments.domain.verify", operation_id="environments_domain_verify", method="POST", path="/workspace/environments/{environment_id}/domain/verify", path_params=("environment_id",), scope="environments:write", danger="write", approval=True, body=False, pagination=None), - "environments.list": OperationSpec(action="environments.list", operation_id="environments_list", method="GET", path="/workspace/environments", path_params=(), scope="workspace:read", danger=None, approval=False, body=False, pagination="page"), + "environments.list": OperationSpec(action="environments.list", operation_id="environments_list", method="GET", path="/workspace/environments", path_params=(), scope="workspace:read", danger=None, approval=True, body=False, pagination="page"), "keys.environment.create": OperationSpec(action="keys.environment.create", operation_id="keys_environment_create", method="POST", path="/workspace/environments/{environment_id}/keys", path_params=("environment_id",), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), "keys.environment.revoke": OperationSpec(action="keys.environment.revoke", operation_id="keys_environment_revoke", method="DELETE", path="/workspace/environments/{environment_id}/keys/{id}", path_params=("environment_id", "id"), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), "keys.workspace.create": OperationSpec(action="keys.workspace.create", operation_id="keys_workspace_create", method="POST", path="/workspace/keys", path_params=(), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), "keys.workspace.list": OperationSpec(action="keys.workspace.list", operation_id="keys_workspace_list", method="GET", path="/workspace/keys", path_params=(), scope="workspace:read", danger="read", approval=True, body=False, pagination="page"), "keys.workspace.revoke": OperationSpec(action="keys.workspace.revoke", operation_id="keys_workspace_revoke", method="DELETE", path="/workspace/keys/{id}", path_params=("id",), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), - "projects.create": OperationSpec(action="projects.create", operation_id="projects_create", method="POST", path="/workspace/projects", path_params=(), scope="projects:write", danger=None, approval=False, body=True, pagination=None), - "projects.list": OperationSpec(action="projects.list", operation_id="projects_list", method="GET", path="/workspace/projects", path_params=(), scope="workspace:read", danger=None, approval=False, body=False, pagination=None), + "projects.create": OperationSpec(action="projects.create", operation_id="projects_create", method="POST", path="/workspace/projects", path_params=(), scope="projects:write", danger=None, approval=True, body=True, pagination=None), + "projects.list": OperationSpec(action="projects.list", operation_id="projects_list", method="GET", path="/workspace/projects", path_params=(), scope="workspace:read", danger=None, approval=True, body=False, pagination=None), "projects.reactivate": OperationSpec(action="projects.reactivate", operation_id="projects_reactivate", method="POST", path="/workspace/projects/{id}/reactivate", path_params=("id",), scope="projects:write", danger="write", approval=True, body=False, pagination=None), "projects.rename": OperationSpec(action="projects.rename", operation_id="projects_rename", method="PATCH", path="/workspace/projects/{id}", path_params=("id",), scope="projects:write", danger="write", approval=True, body=True, pagination=None), "projects.suspend": OperationSpec(action="projects.suspend", operation_id="projects_suspend", method="POST", path="/workspace/projects/{id}/suspend", path_params=("id",), scope="projects:write", danger="write", approval=True, body=False, pagination=None), "projects.verification.resend": OperationSpec(action="projects.verification.resend", operation_id="projects_verification_resend", method="POST", path="/workspace/projects/verification/resend", path_params=(), scope="projects:write", danger="write", approval=True, body=False, pagination=None), "team.environment_access": OperationSpec(action="team.environment_access", operation_id="team_environment_access", method="PUT", path="/workspace/members/{id}/access", path_params=("id",), scope="team:write", danger="write", approval=True, body=True, pagination=None), - "team.invitations.list": OperationSpec(action="team.invitations.list", operation_id="team_invitations_list", method="GET", path="/workspace/invitations", path_params=(), scope="team:read", danger=None, approval=False, body=False, pagination=None), - "team.invitations.resend": OperationSpec(action="team.invitations.resend", operation_id="team_invitations_resend", method="POST", path="/workspace/invitations/{id}/resend", path_params=("id",), scope="team:write", danger=None, approval=False, body=False, pagination=None), - "team.invitations.revoke": OperationSpec(action="team.invitations.revoke", operation_id="team_invitations_revoke", method="DELETE", path="/workspace/invitations/{id}", path_params=("id",), scope="team:write", danger=None, approval=False, body=False, pagination=None), - "team.invite": OperationSpec(action="team.invite", operation_id="team_invite", method="POST", path="/workspace/members", path_params=(), scope="team:write", danger=None, approval=False, body=True, pagination=None), - "team.list": OperationSpec(action="team.list", operation_id="team_list", method="GET", path="/workspace/members", path_params=(), scope="team:read", danger=None, approval=False, body=False, pagination="page"), + "team.invitations.list": OperationSpec(action="team.invitations.list", operation_id="team_invitations_list", method="GET", path="/workspace/invitations", path_params=(), scope="team:read", danger=None, approval=True, body=False, pagination=None), + "team.invitations.resend": OperationSpec(action="team.invitations.resend", operation_id="team_invitations_resend", method="POST", path="/workspace/invitations/{id}/resend", path_params=("id",), scope="team:write", danger=None, approval=True, body=False, pagination=None), + "team.invitations.revoke": OperationSpec(action="team.invitations.revoke", operation_id="team_invitations_revoke", method="DELETE", path="/workspace/invitations/{id}", path_params=("id",), scope="team:write", danger=None, approval=True, body=False, pagination=None), + "team.invite": OperationSpec(action="team.invite", operation_id="team_invite", method="POST", path="/workspace/members", path_params=(), scope="team:write", danger=None, approval=True, body=True, pagination=None), + "team.list": OperationSpec(action="team.list", operation_id="team_list", method="GET", path="/workspace/members", path_params=(), scope="team:read", danger=None, approval=True, body=False, pagination="page"), "team.remove": OperationSpec(action="team.remove", operation_id="team_remove", method="DELETE", path="/workspace/members/{id}", path_params=("id",), scope="team:write", danger="destructive", approval=True, body=False, pagination=None), "team.role": OperationSpec(action="team.role", operation_id="team_role", method="PATCH", path="/workspace/members/{id}/role", path_params=("id",), scope="team:write", danger="write", approval=True, body=True, pagination=None), "team.transfer_ownership": OperationSpec(action="team.transfer_ownership", operation_id="team_transfer_ownership", method="POST", path="/workspace/members/{id}/transfer-ownership", path_params=("id",), scope="team:write", danger="critical", approval=True, body=False, pagination=None), - "workspace.get": OperationSpec(action="workspace.get", operation_id="workspace_get", method="GET", path="/workspace", path_params=(), scope="workspace:read", danger=None, approval=False, body=False, pagination=None), + "workspace.get": OperationSpec(action="workspace.get", operation_id="workspace_get", method="GET", path="/workspace", path_params=(), scope="workspace:read", danger=None, approval=True, body=False, pagination=None), "workspace.settings.update": OperationSpec(action="workspace.settings.update", operation_id="workspace_settings_update", method="PATCH", path="/workspace", path_params=(), scope="settings:write", danger="write", approval=True, body=True, pagination=None), } @@ -404,7 +406,13 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core self.domain = EnvironmentsDomainMethods(core) - def create(self, body: EnvironmentsCreateBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CreatedEnvironment]: + @overload + def create(self, body: EnvironmentsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CreatedEnvironment]: ... + + @overload + def create(self, body: EnvironmentsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CreatedEnvironment] | PendingApprovalResult[CreatedEnvironment]: ... + + def create(self, body: EnvironmentsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[CreatedEnvironment] | PendingApprovalResult[CreatedEnvironment]: """Create an environment Requires scope `environments:write` and the `manage-environments` capability @@ -417,18 +425,26 @@ def create(self, body: EnvironmentsCreateBody, *, idempotency_key: str | None = ``POST /workspace/environments`` · action ``environments.create`` Scope ``environments:write`` · danger: critical. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["environments.create"], (), body, idempotency_key=idempotency_key, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["environments.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: EnvironmentsListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentsListData]: ... - def list(self, query: EnvironmentsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentsListData]: + @overload + def list(self, query: EnvironmentsListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentsListData] | PendingApprovalResult[EnvironmentsListData]: ... + + def list(self, query: EnvironmentsListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[EnvironmentsListData] | PendingApprovalResult[EnvironmentsListData]: """List environments Requires scope `workspace:read` (any role). ``GET /workspace/environments`` · action ``environments.list`` Scope ``workspace:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["environments.list"], (), query, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["environments.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) def list_all(self, query: EnvironmentsListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Environment]: """Every item of ``environments.list``, fetching pages as the iteration reaches them.""" @@ -527,7 +543,7 @@ def revoke(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key def revoke(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: - """Revoke a workspace key, and every key it minted; whatever uses them stops immediately. + """Revoke a workspace key, and every key it minted on either plane (workspace keys and environment management keys, all the way down); whatever uses them stops immediately. Requires scope `keys:write` and a role that may `manage-environments` and `manage-members`. Danger: destructive. @@ -554,12 +570,12 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core @overload - def resend(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsVerificationResendData]: ... + def resend(self, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[VerificationResend]: ... @overload - def resend(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsVerificationResendData] | PendingApprovalResult[ProjectsVerificationResendData]: ... + def resend(self, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[VerificationResend] | PendingApprovalResult[VerificationResend]: ... - def resend(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsVerificationResendData] | PendingApprovalResult[ProjectsVerificationResendData]: + def resend(self, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[VerificationResend] | PendingApprovalResult[VerificationResend]: """Re-send the signup confirmation the workspace's first environment is waiting on, to the owner's address on file. Requires scope `projects:write` and a role that may `manage-environments`. Danger: write. @@ -578,25 +594,39 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core self.verification = ProjectsVerificationMethods(core) - def create(self, body: ProjectsCreateBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: + @overload + def create(self, body: ProjectsCreateBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: ... + + @overload + def create(self, body: ProjectsCreateBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: ... + + def create(self, body: ProjectsCreateBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project] | PendingApprovalResult[Project]: """Create a project Stand up another independently-billed IdP product. Requires scope `projects:write` and the `manage-environments` capability (owner/admin/developer). ``POST /workspace/projects`` · action ``projects.create`` Scope ``projects:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["projects.create"], (), body, idempotency_key=idempotency_key, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["projects.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsListData]: ... - def list(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsListData]: + @overload + def list(self, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsListData] | PendingApprovalResult[ProjectsListData]: ... + + def list(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[ProjectsListData] | PendingApprovalResult[ProjectsListData]: """List projects - Requires scope `workspace:read` (any role). The organization's projects (IdP products). Each carries its own plan and environment allowance. + Requires scope `workspace:read` (any role). The workspace's projects (IdP products). Each carries its own plan and environment allowance. ``GET /workspace/projects`` · action ``projects.list`` Scope ``workspace:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["projects.list"], (), None, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["projects.list"], (), None, approval=approval, approval_id=approval_id, headers=headers) @overload def reactivate(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Project]: ... @@ -656,17 +686,30 @@ class TeamInvitationsMethods: def __init__(self, core: ManagementTransport) -> None: self._core = core - def list(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamInvitationsListData]: + @overload + def list(self, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamInvitationsListData]: ... + + @overload + def list(self, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamInvitationsListData] | PendingApprovalResult[TeamInvitationsListData]: ... + + def list(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamInvitationsListData] | PendingApprovalResult[TeamInvitationsListData]: """List pending invitations Requires scope `team:read` and the `read-members` capability. The team's invitations nobody has accepted yet, newest first (at most 100). ``GET /workspace/invitations`` · action ``team.invitations.list`` Scope ``team:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["team.invitations.list"], (), None, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["team.invitations.list"], (), None, approval=approval, approval_id=approval_id, headers=headers) + + @overload + def resend(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def resend(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... - def resend(self, id: str, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: + def resend(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: """Re-send an invitation Requires scope `team:write` and the `manage-members` capability. Mails a fresh link; the earlier link stops @@ -675,18 +718,26 @@ def resend(self, id: str, *, idempotency_key: str | None = None, headers: Mappin ``POST /workspace/invitations/{id}/resend`` · action ``team.invitations.resend`` Scope ``team:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["team.invitations.resend"], (id,), None, idempotency_key=idempotency_key, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["team.invitations.resend"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) - def revoke(self, id: str, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: + @overload + def revoke(self, id: str, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None]: ... + + @overload + def revoke(self, id: str, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: ... + + def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[None] | PendingApprovalResult[None]: """Withdraw an invitation - Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending on this organization is a `404`. + Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending in this workspace is a `404`. ``DELETE /workspace/invitations/{id}`` · action ``team.invitations.revoke`` Scope ``team:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["team.invitations.revoke"], (id,), None, idempotency_key=idempotency_key, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["team.invitations.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) class TeamMethods: @@ -713,11 +764,17 @@ def environment_access(self, id: str, body: TeamEnvironmentAccessBody, *, approv """ return self._core.call(WORKSPACE_OPERATIONS["team.environment_access"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) - def invite(self, body: TeamInviteBody, *, idempotency_key: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: + @overload + def invite(self, body: TeamInviteBody, *, approval: Literal["wait"] = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member]: ... + + @overload + def invite(self, body: TeamInviteBody, *, approval: Literal["return"], idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: ... + + def invite(self, body: TeamInviteBody, *, approval: ApprovalMode = "wait", idempotency_key: str | None = None, approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Member] | PendingApprovalResult[Member]: """Invite a member Requires scope `team:write` and the `manage-members` capability (owner/admin). Invites the address onto the - organization's team — the same invitation the console's Team page sends: a mail naming + workspace's team — the same invitation the console's Team page sends: a mail naming this key as the inviter and the role, with a signed link on which the invitee sets a password and is signed in. Owner is never invited; ownership is transferred. An earlier pending invitation for the same address is superseded. List, re-send and @@ -725,18 +782,26 @@ def invite(self, body: TeamInviteBody, *, idempotency_key: str | None = None, he ``POST /workspace/members`` · action ``team.invite`` Scope ``team:write``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["team.invite"], (), body, idempotency_key=idempotency_key, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["team.invite"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) + + @overload + def list(self, query: TeamListQuery | None = None, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamListData]: ... + + @overload + def list(self, query: TeamListQuery | None = None, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamListData] | PendingApprovalResult[TeamListData]: ... - def list(self, query: TeamListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamListData]: + def list(self, query: TeamListQuery | None = None, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[TeamListData] | PendingApprovalResult[TeamListData]: """List members Requires scope `team:read` and the `read-members` capability (owner/admin/viewer). The roster is PII — a developer key is refused. ``GET /workspace/members`` · action ``team.list`` Scope ``team:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["team.list"], (), query, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["team.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) def list_all(self, query: TeamListQuery | None = None, *, headers: Mapping[str, str] | None = None) -> Iterator[Member]: """Every item of ``team.list``, fetching pages as the iteration reaches them.""" @@ -825,15 +890,22 @@ def __init__(self, core: ManagementTransport) -> None: self._core = core self.settings = WorkspaceSettingsMethods(core) - def get(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: - """Get the organization + @overload + def get(self, *, approval: Literal["wait"] = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization]: ... - Requires scope `workspace:read` (any role). Returns the organization's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer). + @overload + def get(self, *, approval: Literal["return"], approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: ... + + def get(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None, headers: Mapping[str, str] | None = None) -> ApiResponse[Organization] | PendingApprovalResult[Organization]: + """Get the workspace + + Requires scope `workspace:read` (any role). Returns the workspace's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer). ``GET /workspace`` · action ``workspace.get`` Scope ``workspace:read``. + May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ - return self._core.call(WORKSPACE_OPERATIONS["workspace.get"], (), None, headers=headers) + return self._core.call(WORKSPACE_OPERATIONS["workspace.get"], (), None, approval=approval, approval_id=approval_id, headers=headers) # ── Client ──────────────────────────────────────────────────────────────────────────────────────── @@ -842,7 +914,7 @@ def get(self, *, headers: Mapping[str, str] | None = None) -> ApiResponse[Organi class WorkspaceClient(ManagementClient): """Cbox ID — Workspace Management API. - The **global organization-management plane** — above every environment. Manage the + The **workspace plane** — the customer's own Cbox account, above every environment it owns. Manage the workspace, its **projects** (IdP products, each a billing anchor) and their environments, its team, and its keys with a workspace key. diff --git a/tests/fixtures/management_surface.txt b/tests/fixtures/management_surface.txt index 8c27b88..8b5d91d 100644 --- a/tests/fixtures/management_surface.txt +++ b/tests/fixtures/management_surface.txt @@ -10,29 +10,29 @@ environment.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovals environment.api_keys.list(organization_id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[ApiKeysListData] | PendingApprovalResult[ApiKeysListData] environment.api_keys.list_all(organization_id, query=, *, headers=) -> Iterator[ApiKey] environment.api_keys.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] -environment.apis.create(body, *, idempotency_key=, headers=) -> ApiResponse[Api] -environment.apis.delete(id, *, idempotency_key=, headers=) -> ApiResponse[None] -environment.apis.get(id, *, headers=) -> ApiResponse[Api] -environment.apis.list(query=, *, headers=) -> ApiResponse[ApisListData] +environment.apis.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Api] | PendingApprovalResult[Api] +environment.apis.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +environment.apis.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[Api] | PendingApprovalResult[Api] +environment.apis.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[ApisListData] | PendingApprovalResult[ApisListData] environment.apis.list_all(query=, *, headers=) -> Iterator[Api] -environment.apis.update(id, body=, *, idempotency_key=, headers=) -> ApiResponse[Api] +environment.apis.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Api] | PendingApprovalResult[Api] environment.apis.scopes.define(id, key, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Api] | PendingApprovalResult[Api] environment.apis.scopes.remove(id, key, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.approvals.deny(request_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.approvals.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[ApprovalsListData] | PendingApprovalResult[ApprovalsListData] environment.approvals.list_all(query=, *, headers=) -> Iterator[AgentRequest] -environment.apps.blueprint(id, *, headers=) -> ApiResponse[AppBlueprint] +environment.apps.blueprint(id, *, approval=, approval_id=, headers=) -> ApiResponse[AppBlueprint] | PendingApprovalResult[AppBlueprint] environment.apps.copy(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] -environment.apps.create(body=, *, idempotency_key=, headers=) -> ApiResponse[App] +environment.apps.create(body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] environment.apps.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.apps.get(id, *, approval=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] -environment.apps.list(query=, *, headers=) -> ApiResponse[AppsListData] +environment.apps.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[AppsListData] | PendingApprovalResult[AppsListData] environment.apps.list_all(query=, *, headers=) -> Iterator[App] environment.apps.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] environment.apps.manifest.set(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] -environment.apps.manifest.sync(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AppsManifestSyncData] | PendingApprovalResult[AppsManifestSyncData] +environment.apps.manifest.sync(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ManifestSync] | PendingApprovalResult[ManifestSync] environment.apps.scopes.set(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] -environment.apps.secrets.list(id, *, headers=) -> ApiResponse[AppsSecretsListData] +environment.apps.secrets.list(id, *, approval=, approval_id=, headers=) -> ApiResponse[AppsSecretsListData] | PendingApprovalResult[AppsSecretsListData] environment.apps.secrets.revoke(id, secret_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.apps.secrets.rotate(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[AppSecret] | PendingApprovalResult[AppSecret] environment.apps.settings.api_key_prefix(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[App] | PendingApprovalResult[App] @@ -93,14 +93,14 @@ environment.invitations.list_all(organization_id, query=, *, headers=) -> Iterat environment.invitations.resend(organization_id, invitation_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation] environment.invitations.revoke(organization_id, invitation_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.invitations.send(organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Invitation] | PendingApprovalResult[Invitation] -environment.keys.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[KeysCreateData] | PendingApprovalResult[KeysCreateData] +environment.keys.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ManagementKey] | PendingApprovalResult[ManagementKey] environment.keys.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[KeysListData] | PendingApprovalResult[KeysListData] -environment.keys.list_all(query=, *, headers=) -> Iterator[KeysListItem] +environment.keys.list_all(query=, *, headers=) -> Iterator[ManagementKey] environment.keys.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] -environment.keys.rotate(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[KeysRotateData] | PendingApprovalResult[KeysRotateData] +environment.keys.rotate(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ManagementKey] | PendingApprovalResult[ManagementKey] environment.legacy_login.approve(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin] environment.legacy_login.get(*, approval=, approval_id=, headers=) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin] -environment.legacy_login.probe(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLoginProbeData] | PendingApprovalResult[LegacyLoginProbeData] +environment.legacy_login.probe(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLoginProbe] | PendingApprovalResult[LegacyLoginProbe] environment.legacy_login.revoke(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LegacyLogin] | PendingApprovalResult[LegacyLogin] environment.log_streams.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream] environment.log_streams.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] @@ -108,7 +108,7 @@ environment.log_streams.get(id, *, approval=, approval_id=, headers=) -> ApiResp environment.log_streams.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[LogStreamsListData] | PendingApprovalResult[LogStreamsListData] environment.log_streams.list_all(query=, *, headers=) -> Iterator[LogStream] environment.log_streams.test(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStreamTest] | PendingApprovalResult[LogStreamTest] -environment.log_streams.update(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream] +environment.log_streams.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[LogStream] | PendingApprovalResult[LogStream] environment.members.add(organization_id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] environment.members.list(organization_id, query=, *, approval=, approval_id=, headers=) -> ApiResponse[MembersListData] | PendingApprovalResult[MembersListData] environment.members.list_all(organization_id, query=, *, headers=) -> Iterator[Member] @@ -132,6 +132,8 @@ environment.organizations.domains.list(organization_id, *, approval=, approval_i environment.organizations.domains.remove(organization_id, domain_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.organizations.domains.verify(organization_id, domain_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[OrganizationDomain] | PendingApprovalResult[OrganizationDomain] environment.organizations.portal_links.create(organization_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PortalLink] | PendingApprovalResult[PortalLink] +environment.organizations.portal_links.list(organization_id, *, approval=, approval_id=, headers=) -> ApiResponse[OrganizationsPortalLinksListData] | PendingApprovalResult[OrganizationsPortalLinksListData] +environment.organizations.portal_links.revoke(organization_id, id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.permissions.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Permission] | PendingApprovalResult[Permission] environment.permissions.delete(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.permissions.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[PermissionsListData] | PendingApprovalResult[PermissionsListData] @@ -159,7 +161,7 @@ environment.saml_apps.update(id, body=, *, approval=, idempotency_key=, approval environment.signin.policy.get(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy] environment.signin.policy.inherit(organization_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.signin.policy.update(body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SignInPolicy] | PendingApprovalResult[SignInPolicy] -environment.signin.self_service_signup.set(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SigninSelfServiceSignupSetData] | PendingApprovalResult[SigninSelfServiceSignupSetData] +environment.signin.self_service_signup.set(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[SelfServiceSignup] | PendingApprovalResult[SelfServiceSignup] environment.signin.social.delete(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] environment.signin.social.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[SigninSocialListData] | PendingApprovalResult[SigninSocialListData] environment.signin.social.list_all(query=, *, headers=) -> Iterator[SocialProvider] @@ -228,9 +230,10 @@ environment.webhooks.pause(id, *, approval=, idempotency_key=, approval_id=, hea environment.webhooks.resume(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] environment.webhooks.update(id, body=, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] environment.webhooks.secret.rotate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] +environment.webhooks.signature_scheme.change(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Webhook] | PendingApprovalResult[Webhook] workspace.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovalsGetData] -workspace.environments.create(body, *, idempotency_key=, headers=) -> ApiResponse[CreatedEnvironment] -workspace.environments.list(query=, *, headers=) -> ApiResponse[EnvironmentsListData] +workspace.environments.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[CreatedEnvironment] | PendingApprovalResult[CreatedEnvironment] +workspace.environments.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[EnvironmentsListData] | PendingApprovalResult[EnvironmentsListData] workspace.environments.list_all(query=, *, headers=) -> Iterator[Environment] workspace.environments.domain.remove(environment_id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] workspace.environments.domain.request(environment_id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[CustomDomain] | PendingApprovalResult[CustomDomain] @@ -241,23 +244,23 @@ workspace.keys.workspace.create(body, *, approval=, idempotency_key=, approval_i workspace.keys.workspace.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[KeysWorkspaceListData] | PendingApprovalResult[KeysWorkspaceListData] workspace.keys.workspace.list_all(query=, *, headers=) -> Iterator[WorkspaceKey] workspace.keys.workspace.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] -workspace.projects.create(body, *, idempotency_key=, headers=) -> ApiResponse[Project] -workspace.projects.list(*, headers=) -> ApiResponse[ProjectsListData] +workspace.projects.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] +workspace.projects.list(*, approval=, approval_id=, headers=) -> ApiResponse[ProjectsListData] | PendingApprovalResult[ProjectsListData] workspace.projects.reactivate(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] workspace.projects.rename(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] workspace.projects.suspend(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Project] | PendingApprovalResult[Project] -workspace.projects.verification.resend(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[ProjectsVerificationResendData] | PendingApprovalResult[ProjectsVerificationResendData] +workspace.projects.verification.resend(*, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[VerificationResend] | PendingApprovalResult[VerificationResend] workspace.team.environment_access(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] -workspace.team.invite(body, *, idempotency_key=, headers=) -> ApiResponse[Member] -workspace.team.list(query=, *, headers=) -> ApiResponse[TeamListData] +workspace.team.invite(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +workspace.team.list(query=, *, approval=, approval_id=, headers=) -> ApiResponse[TeamListData] | PendingApprovalResult[TeamListData] workspace.team.list_all(query=, *, headers=) -> Iterator[Member] workspace.team.remove(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] workspace.team.role(id, body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] workspace.team.transfer_ownership(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] -workspace.team.invitations.list(*, headers=) -> ApiResponse[TeamInvitationsListData] -workspace.team.invitations.resend(id, *, idempotency_key=, headers=) -> ApiResponse[Member] -workspace.team.invitations.revoke(id, *, idempotency_key=, headers=) -> ApiResponse[None] -workspace.workspace.get(*, headers=) -> ApiResponse[Organization] +workspace.team.invitations.list(*, approval=, approval_id=, headers=) -> ApiResponse[TeamInvitationsListData] | PendingApprovalResult[TeamInvitationsListData] +workspace.team.invitations.resend(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Member] | PendingApprovalResult[Member] +workspace.team.invitations.revoke(id, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[None] | PendingApprovalResult[None] +workspace.workspace.get(*, approval=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] workspace.workspace.settings.update(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[Organization] | PendingApprovalResult[Organization] platform.action_approvals.get(id, *, headers=) -> ApiResponse[ActionApprovalsGetData] platform.environments.create(body, *, approval=, idempotency_key=, approval_id=, headers=) -> ApiResponse[PlatformEnvironment] | PendingApprovalResult[PlatformEnvironment] diff --git a/tests/test_management.py b/tests/test_management.py index 47851b7..4c0b6d8 100644 --- a/tests/test_management.py +++ b/tests/test_management.py @@ -16,7 +16,9 @@ from typing_extensions import assert_type from cbox_id.management import ( + ACCOUNT_OPERATIONS, ENVIRONMENT_OPERATIONS, + PLATFORM_OPERATIONS, WORKSPACE_OPERATIONS, AccountClient, ApiResponse, @@ -645,6 +647,26 @@ def test_names_methods_after_x_action_with_the_specs_method_path_scope_and_dange assert ENVIRONMENT_OPERATIONS["audit_logs.verify"].scope == "audit_logs:read" +def test_every_action_carries_its_scope() -> None: + """From ``x-scope``, or the description's "Requires scope" for a hand-written route.""" + tables = [ENVIRONMENT_OPERATIONS, WORKSPACE_OPERATIONS, PLATFORM_OPERATIONS, ACCOUNT_OPERATIONS] + actions = [op for table in tables for op in table.values() if op.action is not None] + + assert actions + assert [op.action for op in actions if op.scope is None] == [] + + scheme = ENVIRONMENT_OPERATIONS["webhooks.signature_scheme.change"] + assert (scheme.method, scheme.path, scheme.scope, scheme.danger) == ( + "POST", + "/webhooks/{id}/signature-scheme", + "webhooks:write", + "destructive", + ) + links = ENVIRONMENT_OPERATIONS["organizations.portal_links.revoke"] + assert links.method == "DELETE" + assert links.path_params == ("organization_id", "id") + + def test_generated_code_is_exactly_what_the_generator_makes_of_the_vendored_specs() -> None: """Regenerate with ``python -m scripts.generate_management``.""" for path, result in generate_all().items(): From d1945d411c1227c1cda0b5bf15753966dd9b121e Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 15:11:55 +0200 Subject: [PATCH 6/8] feat(webhooks): verify Standard Webhooks signatures verify_standard_webhook() checks webhook-id / webhook-timestamp / webhook-signature for endpoints on the standard_webhooks scheme, with a whsec_ secret or the endpoint's hex Cbox secret, and is tested against the specification's vector. README: install line names v0.9.0. --- CHANGELOG.md | 7 +++- README.md | 23 ++++++++++- src/cbox_id/__init__.py | 3 +- src/cbox_id/webhook.py | 67 ++++++++++++++++++++++++++++++ tests/test_standard_webhook.py | 74 ++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 tests/test_standard_webhook.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ec6c5..67905fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ by their tags and commit history. ### Added - `cbox_id.management`: typed clients for Cbox ID's management planes, generated from the - OpenAPI documents the server publishes. `EnvironmentClient` (an environment's own host, + OpenAPI documents the server publishes (vendored from Cbox ID's wave-7 specs). `EnvironmentClient` (an environment's own host, `cbid_env_…` key or delegated token), `WorkspaceClient` (`cbid_ws_…` key), `PlatformClient` (operator token) and `AccountClient` (a person's own token). Methods are named after the server's actions (`env.apps.secrets.rotate(id, body)`, @@ -38,12 +38,17 @@ by their tags and commit history. `DPoP-Nonce` challenge handling. - Operation tables (`ENVIRONMENT_OPERATIONS`, …) with each action's method, path, scope, danger and whether it can be held for approval. +- `verify_standard_webhook()`: verifies deliveries from an endpoint on the + `standard_webhooks` signature scheme (`webhook-id` / `webhook-timestamp` / + `webhook-signature`), with a `whsec_…` secret or the endpoint's hex Cbox secret. Tested + against the Standard Webhooks specification's vector. - `python -m scripts.generate_management` regenerates the clients from the vendored specs in `openapi/`; `--fetch =` refreshes a spec from a running server first, and `--check` fails when the generated code is stale. So does the test suite. ### Changed +- README: the install line pinned v0.8.0; it now names v0.9.0. - New runtime dependency: `typing-extensions>=4.7` (`NotRequired` and `TypedDict` on Python 3.10). The `dev` extra adds `pyyaml` and `types-PyYAML` for the generator. diff --git a/README.md b/README.md index 3bd4796..0737472 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ request abstraction invented to avoid them. > below works; you install it from source. ```bash -pip install git+https://github.com/cboxdk/id-python@v0.8.0 +pip install git+https://github.com/cboxdk/id-python@v0.9.0 ``` ## Log in users @@ -345,6 +345,26 @@ ok = verify_webhook( ) ``` +An endpoint switched to the `standard_webhooks` signature scheme sends `webhook-id`, +`webhook-timestamp` and `webhook-signature` instead, verifiable with any +[Standard Webhooks](https://www.standardwebhooks.com) library or with: + +```python +from cbox_id import verify_standard_webhook + +ok = verify_standard_webhook( + raw_body, # the exact bytes received + webhook_id=request.headers.get("webhook-id"), + webhook_timestamp=request.headers.get("webhook-timestamp"), + webhook_signature=request.headers.get("webhook-signature"), + secret=os.environ["CBOX_ID_WEBHOOK_SECRET"], # whsec_…, or the endpoint's hex secret +) +``` + +Switching scheme keeps the secret: a hex Cbox secret is used as `whsec_` + base64 of itself, +and both forms verify here. Update the receiver before you switch the endpoint +(`env.webhooks.signature_scheme.change(id, {"signature_scheme": "standard_webhooks"})`). + ## Management API `cbox_id.management` is a typed client for Cbox ID's management planes. It is generated from @@ -499,6 +519,7 @@ from collections.abc import Sequence from cbox_id.management import AuditLogEventInput, AuditLogger + def report(exc: Exception, batch: Sequence[AuditLogEventInput]) -> None: log.warning("audit flush failed (%d events kept for the next one): %s", len(batch), exc) diff --git a/src/cbox_id/__init__.py b/src/cbox_id/__init__.py index 2747094..f6f0052 100644 --- a/src/cbox_id/__init__.py +++ b/src/cbox_id/__init__.py @@ -35,7 +35,7 @@ RefreshedTokens, ) from .pkce import challenge, create_verifier, random_token -from .webhook import verify_webhook +from .webhook import verify_standard_webhook, verify_webhook __all__ = [ "FrontendClient", @@ -72,5 +72,6 @@ "permissions", "random_token", "roles", + "verify_standard_webhook", "verify_webhook", ] diff --git a/src/cbox_id/webhook.py b/src/cbox_id/webhook.py index d8528d6..373540b 100644 --- a/src/cbox_id/webhook.py +++ b/src/cbox_id/webhook.py @@ -2,6 +2,8 @@ from __future__ import annotations +import base64 +import binascii import hashlib import hmac import time @@ -47,3 +49,68 @@ def verify_webhook( ).hexdigest() return hmac.compare_digest(expected, signature) + + +def _standard_webhooks_key(secret: str) -> bytes: + """The HMAC key a Standard Webhooks secret names. + + A ``whsec_`` secret is base64 of the key. Anything else is taken as a Cbox secret, + whose key is its own characters — the server signs a 64-hex Cbox secret under + ``standard_webhooks`` as ``whsec_`` + base64 of that hex string, so both forms of the + same secret verify the same deliveries. + """ + if secret.startswith("whsec_"): + return base64.b64decode(secret[len("whsec_") :], validate=True) + + return secret.encode("utf-8") + + +def verify_standard_webhook( + payload: str | bytes, + *, + webhook_id: str | None, + webhook_timestamp: str | None, + webhook_signature: str | None, + secret: str, + tolerance_seconds: int = 300, + now: float | None = None, +) -> bool: + """Verify a delivery signed with the ``standard_webhooks`` scheme. + + Pass the ``webhook-id``, ``webhook-timestamp`` and ``webhook-signature`` headers and the + RAW request body. The signature is base64 HMAC-SHA256 over ``"{id}.{timestamp}.{body}"``; + the header may list several space-separated ``v1,`` entries (during a secret + rotation) and any one matching is enough. ``secret`` is the endpoint's ``whsec_…`` + secret, or its Cbox hex secret. Returns ``False`` (never raises) on any problem. + + Endpoints on the default ``cbox`` scheme send ``X-Cbox-Signature`` instead: use + :func:`verify_webhook` for those. + """ + if not webhook_id or not webhook_timestamp or not webhook_signature: + return False + + if not webhook_timestamp.isdigit(): + return False + + current = time.time() if now is None else now + if abs(current - int(webhook_timestamp)) > tolerance_seconds: + return False + + try: + key = _standard_webhooks_key(secret) + except (ValueError, binascii.Error): + return False + + body = payload.encode("utf-8") if isinstance(payload, str) else payload + signed = f"{webhook_id}.{webhook_timestamp}.".encode() + body + expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode("ascii") + matched = False + + for entry in webhook_signature.split(): + version, _, signature = entry.partition(",") + + # Compare every candidate, so the time taken does not say which entry matched. + if version == "v1" and hmac.compare_digest(expected, signature): + matched = True + + return matched diff --git a/tests/test_standard_webhook.py b/tests/test_standard_webhook.py new file mode 100644 index 0000000..f333a66 --- /dev/null +++ b/tests/test_standard_webhook.py @@ -0,0 +1,74 @@ +"""Standard Webhooks verification, against the specification's own test vector.""" + +from __future__ import annotations + +import base64 +import hashlib +import hmac + +from cbox_id import verify_standard_webhook + +# https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md +SECRET = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" +MSG_ID = "msg_p5jXN8AQM9LWM0D4loKWxJek" +TIMESTAMP = "1614265330" +BODY = '{"test": 2432232314}' +SIGNATURE = "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=" +NOW = 1614265330 + + +def verify(signature: str = SIGNATURE, **overrides: object) -> bool: + options: dict[str, object] = { + "webhook_id": MSG_ID, + "webhook_timestamp": TIMESTAMP, + "webhook_signature": signature, + "secret": SECRET, + "now": NOW, + **overrides, + } + payload = options.pop("payload", BODY) + return verify_standard_webhook(payload, **options) # type: ignore[arg-type] + + +def test_accepts_the_spec_vector() -> None: + assert verify() is True + assert verify(payload=BODY.encode()) is True + + +def test_accepts_any_matching_entry_of_several() -> None: + assert verify(f"v1,bm90IGl0 {SIGNATURE} v2,ignored") is True + + +def test_refuses_a_changed_body_id_timestamp_or_secret() -> None: + assert verify(payload='{"test": 2432232315}') is False + assert verify(webhook_id="msg_other") is False + assert verify(webhook_timestamp="1614265331", now=1614265331) is False + assert verify(secret="whsec_" + base64.b64encode(b"another key").decode()) is False + + +def test_refuses_a_stale_or_future_timestamp() -> None: + assert verify(now=NOW + 301) is False + assert verify(now=NOW - 301) is False + assert verify(now=NOW + 300) is True + + +def test_refuses_missing_or_malformed_input_without_raising() -> None: + assert verify("") is False + assert verify("v1") is False + assert verify("v2," + SIGNATURE[3:]) is False + assert verify(webhook_id=None) is False + assert verify(webhook_timestamp="soon") is False + assert verify(secret="whsec_not*base64") is False + + +def test_a_cbox_hex_secret_verifies_like_its_whsec_form() -> None: + """The server signs a hex Cbox secret under standard_webhooks as whsec_ + base64(hex).""" + hex_secret = "ab" * 32 + whsec = "whsec_" + base64.b64encode(hex_secret.encode()).decode() + digest = hmac.new( + hex_secret.encode(), f"{MSG_ID}.{TIMESTAMP}.{BODY}".encode(), hashlib.sha256 + ).digest() + signature = "v1," + base64.b64encode(digest).decode() + + assert verify(signature, secret=hex_secret) is True + assert verify(signature, secret=whsec) is True From 6749b4a6be294d89af60f7345ccbc0f3ed097b29 Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 15:55:32 +0200 Subject: [PATCH 7/8] ci: publish to PyPI with Trusted Publishing when a release is published --- .github/workflows/publish.yml | 48 +++++++++++++++++++++++++++++++++++ README.md | 10 ++++---- 2 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f3a24e2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,48 @@ +name: publish + +# Publishes cbox-id-client to PyPI when a GitHub release is published. +# +# Trusted Publishing (OIDC): no API token is stored anywhere. PyPI trusts this exact +# repository + workflow + environment, configured once on pypi.org under +# "Publishing" (owner cboxdk, repository id-python, workflow publish.yml, environment pypi). +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + # The tag is the version: a release tagged v0.10.0 must build 0.10.0, or nothing ships. + - name: Tag matches the package version + run: | + version=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + test "v${version}" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != pyproject version ${version}"; exit 1; } + - run: pip install -e ".[dev]" build + - run: ruff check . + - run: mypy + - run: pytest -q + - run: python -m build + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/cbox-id-client + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 0737472..c7a6040 100644 --- a/README.md +++ b/README.md @@ -88,14 +88,14 @@ request abstraction invented to avoid them. > Register an application in your environment console — see > [Integrate your app](https://github.com/cboxdk/cbox-id/blob/main/docs/getting-started/integrate-your-app.md). -> **Not on PyPI, and not planned.** `pip install cbox-id-client` installs nothing — this -> package has never been published and there is no release pipeline for it. Everything -> below works; you install it from source. - ```bash -pip install git+https://github.com/cboxdk/id-python@v0.9.0 +pip install cbox-id-client ``` +Published to PyPI from 0.10.0, by the release workflow (Trusted Publishing — no stored +token). Earlier versions were never on PyPI; install those from the tag instead: +`pip install git+https://github.com/cboxdk/id-python@v0.9.0`. + ## Log in users ```python From aa6ff8d6923a063ec89afe3d53f6eea12683647d Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Thu, 8 Oct 2026 16:15:22 +0200 Subject: [PATCH 8/8] =?UTF-8?q?chore(release):=200.10.0=20=E2=80=94=20the?= =?UTF-8?q?=20management=20client,=20against=20cbox-id's=20final=20specs;?= =?UTF-8?q?=20first=20PyPI=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 + openapi/environment.yaml | 85 +++++++++ openapi/workspace.yaml | 20 +++ pyproject.toml | 2 +- .../management/generated/environment.py | 166 +++++++++--------- src/cbox_id/management/generated/workspace.py | 36 ++-- 6 files changed, 209 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67905fc..77eb86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ by their tags and commit history. ## [Unreleased] +## [0.10.0] - 2026-10-08 + ### Added - `cbox_id.management`: typed clients for Cbox ID's management planes, generated from the diff --git a/openapi/environment.yaml b/openapi/environment.yaml index 39a36c0..cdbc403 100644 --- a/openapi/environment.yaml +++ b/openapi/environment.yaml @@ -746,6 +746,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'api_keys:write' + x-danger: destructive /apis: get: tags: @@ -791,6 +793,9 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-action: apis.list + x-scope: 'apis:read' + x-danger: read post: tags: - APIs @@ -868,6 +873,9 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-action: apis.create + x-scope: 'apis:write' + x-danger: write '/apis/{id}': get: tags: @@ -907,6 +915,9 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-action: apis.get + x-scope: 'apis:read' + x-danger: read patch: tags: - APIs @@ -969,6 +980,9 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-action: apis.update + x-scope: 'apis:write' + x-danger: write delete: tags: - APIs @@ -1000,6 +1014,9 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-action: apis.delete + x-scope: 'apis:write' + x-danger: destructive '/apis/{id}/scopes/{key}': put: tags: @@ -1302,6 +1319,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'apps:read' + x-danger: read post: tags: - Applications @@ -1426,6 +1445,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'apps:write' + x-danger: critical '/apps/{id}': delete: tags: @@ -1648,6 +1669,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'apps:read' + x-danger: read '/apps/{id}/copy': post: tags: @@ -1974,6 +1997,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'apps:read' + x-danger: read post: tags: - Applications @@ -6668,6 +6693,8 @@ paths: $ref: '#/components/responses/Forbidden' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:read' + x-danger: read post: operationId: organizations_create x-action: organizations.create @@ -6759,6 +6786,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:write' + x-danger: write '/organizations/{id}': get: operationId: organizations_get @@ -6798,6 +6827,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:read' + x-danger: read patch: operationId: organizations_update x-action: organizations.update @@ -6871,6 +6902,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:write' + x-danger: write delete: operationId: organizations_delete x-action: organizations.delete @@ -6921,6 +6954,8 @@ paths: $ref: '#/components/responses/Conflict' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:write' + x-danger: destructive '/organizations/{id}/reactivate': post: tags: @@ -7097,6 +7132,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:write' + x-danger: critical '/organizations/{organization_id}/api-keys': get: operationId: api_keys_list @@ -7153,6 +7190,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'api_keys:read' + x-danger: read '/organizations/{organization_id}/domains': get: operationId: organizations_domains_list @@ -7198,6 +7237,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'organizations:read' + x-danger: read post: tags: - Organizations @@ -7505,6 +7546,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'invitations:read' + x-danger: read post: operationId: invitations_send x-action: invitations.send @@ -7608,6 +7651,8 @@ paths: $ref: '#/components/responses/TooManyRequests' 503: $ref: '#/components/responses/MailFailed' + x-scope: 'invitations:write' + x-danger: write '/organizations/{organization_id}/invitations/{invitation_id}': delete: operationId: invitations_revoke @@ -7648,6 +7693,8 @@ paths: $ref: '#/components/responses/Conflict' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'invitations:write' + x-danger: destructive '/organizations/{organization_id}/invitations/{invitation_id}/resend': post: operationId: invitations_resend @@ -7704,6 +7751,8 @@ paths: $ref: '#/components/responses/TooManyRequests' 503: $ref: '#/components/responses/MailFailed' + x-scope: 'invitations:write' + x-danger: write '/organizations/{organization_id}/members': get: operationId: members_list @@ -7752,6 +7801,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'members:read' + x-danger: read post: operationId: members_add x-action: members.add @@ -7848,6 +7899,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'members:write' + x-danger: write '/organizations/{organization_id}/members/{user_id}': patch: operationId: members_update @@ -7910,6 +7963,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'members:write' + x-danger: write delete: operationId: members_remove x-action: members.remove @@ -7949,6 +8004,8 @@ paths: $ref: '#/components/responses/Conflict' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'members:write' + x-danger: destructive '/organizations/{organization_id}/members/{user_id}/roles': get: operationId: members_roles_list @@ -7996,6 +8053,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:read' + x-danger: read '/organizations/{organization_id}/members/{user_id}/roles/{role_id}': put: operationId: members_roles_grant @@ -8058,6 +8117,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:write' + x-danger: write delete: operationId: members_roles_revoke x-action: members.roles.revoke @@ -8095,6 +8156,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:write' + x-danger: destructive '/organizations/{organization_id}/portal-links': get: operationId: organizations_portal_links_list @@ -9017,6 +9080,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:read' + x-danger: read post: tags: - Roles @@ -12060,6 +12125,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'support:write' + x-danger: critical '/support-sessions/{id}': delete: tags: @@ -12679,6 +12746,8 @@ paths: $ref: '#/components/responses/Forbidden' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'users:read' + x-danger: read post: operationId: users_create x-action: users.create @@ -12748,6 +12817,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'users:write' + x-danger: write '/users/{id}': get: operationId: users_get @@ -12787,6 +12858,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'users:read' + x-danger: read delete: operationId: users_deactivate x-action: users.deactivate @@ -12830,6 +12903,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'users:write' + x-danger: destructive patch: tags: - Users @@ -12944,6 +13019,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:read' + x-danger: read '/users/{id}/environment-roles/{role_id}': get: operationId: users_environment_roles_get @@ -12987,6 +13064,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:read' + x-danger: read put: operationId: users_environment_roles_grant x-action: users.environment_roles.grant @@ -13040,6 +13119,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:write' + x-danger: critical delete: operationId: users_environment_roles_revoke x-action: users.environment_roles.revoke @@ -13074,6 +13155,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'roles:write' + x-danger: destructive '/users/{id}/erase': post: tags: @@ -13412,6 +13495,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'users:read' + x-danger: read delete: tags: - Users diff --git a/openapi/workspace.yaml b/openapi/workspace.yaml index 78356d3..23a859b 100644 --- a/openapi/workspace.yaml +++ b/openapi/workspace.yaml @@ -100,6 +100,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'workspace:read' + x-danger: read patch: tags: - Workspace @@ -244,6 +246,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'workspace:read' + x-danger: read post: tags: - Environments @@ -343,6 +347,8 @@ paths: message: 'Project [01J…] has reached its plan limit of 2 environment(s).' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'environments:write' + x-danger: critical '/workspace/environments/{environment_id}/domain': delete: tags: @@ -625,6 +631,8 @@ paths: $ref: '#/components/responses/Forbidden' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'team:read' + x-danger: read '/workspace/invitations/{id}': delete: tags: @@ -655,6 +663,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'team:write' + x-danger: destructive '/workspace/invitations/{id}/resend': post: tags: @@ -701,6 +711,8 @@ paths: $ref: '#/components/schemas/Error' 503: $ref: '#/components/responses/MailFailed' + x-scope: 'team:write' + x-danger: write /workspace/keys: post: tags: @@ -917,6 +929,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'team:read' + x-danger: read post: tags: - Team @@ -988,6 +1002,8 @@ paths: $ref: '#/components/responses/TooManyRequests' 503: $ref: '#/components/responses/MailFailed' + x-scope: 'team:write' + x-danger: write '/workspace/members/{id}': delete: tags: @@ -1216,6 +1232,8 @@ paths: $ref: '#/components/responses/NotFound' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'workspace:read' + x-danger: read post: tags: - Projects @@ -1265,6 +1283,8 @@ paths: $ref: '#/components/responses/UnprocessableEntity' 429: $ref: '#/components/responses/TooManyRequests' + x-scope: 'projects:write' + x-danger: write /workspace/projects/verification/resend: post: tags: diff --git a/pyproject.toml b/pyproject.toml index eee22c5..79cdb61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "cbox-id-client" -version = "0.9.0" +version = "0.10.0" description = "Turnkey Cbox ID client for Python — OpenID Connect login (PKCE + id_token verification via JWKS), hosted profile-management redirect, machine tokens, UserInfo, RFC 7662 introspection, and webhook signature verification." readme = "README.md" requires-python = ">=3.10" diff --git a/src/cbox_id/management/generated/environment.py b/src/cbox_id/management/generated/environment.py index 4128991..e9ad72e 100644 --- a/src/cbox_id/management/generated/environment.py +++ b/src/cbox_id/management/generated/environment.py @@ -3057,27 +3057,27 @@ class WebhooksUpdateBody(TypedDict): "access_reviews.items.list": OperationSpec(action="access_reviews.items.list", operation_id="access_reviews_items_list", method="GET", path="/access-reviews/{id}/items", path_params=("id",), scope="governance:read", danger="read", approval=True, body=False, pagination="cursor"), "access_reviews.list": OperationSpec(action="access_reviews.list", operation_id="access_reviews_list", method="GET", path="/access-reviews", path_params=(), scope="governance:read", danger="read", approval=True, body=False, pagination="cursor"), "action_approvals.get": OperationSpec(action=None, operation_id=None, method="GET", path="/action-approvals/{id}", path_params=("id",), scope=None, danger=None, approval=False, body=False, pagination=None), - "api_keys.list": OperationSpec(action="api_keys.list", operation_id="api_keys_list", method="GET", path="/organizations/{organization_id}/api-keys", path_params=("organization_id",), scope="api_keys:read", danger=None, approval=True, body=False, pagination="cursor"), - "api_keys.revoke": OperationSpec(action="api_keys.revoke", operation_id="api_keys_revoke", method="DELETE", path="/api-keys/{id}", path_params=("id",), scope="api_keys:write", danger=None, approval=True, body=False, pagination=None), - "apis.create": OperationSpec(action=None, operation_id=None, method="POST", path="/apis", path_params=(), scope="apis:write", danger=None, approval=True, body=True, pagination=None), - "apis.delete": OperationSpec(action=None, operation_id=None, method="DELETE", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=True, body=False, pagination=None), - "apis.get": OperationSpec(action=None, operation_id=None, method="GET", path="/apis/{id}", path_params=("id",), scope="apis:read", danger=None, approval=True, body=False, pagination=None), - "apis.list": OperationSpec(action=None, operation_id=None, method="GET", path="/apis", path_params=(), scope="apis:read", danger=None, approval=True, body=False, pagination="cursor"), + "api_keys.list": OperationSpec(action="api_keys.list", operation_id="api_keys_list", method="GET", path="/organizations/{organization_id}/api-keys", path_params=("organization_id",), scope="api_keys:read", danger="read", approval=True, body=False, pagination="cursor"), + "api_keys.revoke": OperationSpec(action="api_keys.revoke", operation_id="api_keys_revoke", method="DELETE", path="/api-keys/{id}", path_params=("id",), scope="api_keys:write", danger="destructive", approval=True, body=False, pagination=None), + "apis.create": OperationSpec(action="apis.create", operation_id=None, method="POST", path="/apis", path_params=(), scope="apis:write", danger="write", approval=True, body=True, pagination=None), + "apis.delete": OperationSpec(action="apis.delete", operation_id=None, method="DELETE", path="/apis/{id}", path_params=("id",), scope="apis:write", danger="destructive", approval=True, body=False, pagination=None), + "apis.get": OperationSpec(action="apis.get", operation_id=None, method="GET", path="/apis/{id}", path_params=("id",), scope="apis:read", danger="read", approval=True, body=False, pagination=None), + "apis.list": OperationSpec(action="apis.list", operation_id=None, method="GET", path="/apis", path_params=(), scope="apis:read", danger="read", approval=True, body=False, pagination="cursor"), "apis.scopes.define": OperationSpec(action="apis.scopes.define", operation_id="apis_scopes_define", method="PUT", path="/apis/{id}/scopes/{key}", path_params=("id", "key"), scope="apis:write", danger="write", approval=True, body=True, pagination=None), "apis.scopes.remove": OperationSpec(action="apis.scopes.remove", operation_id="apis_scopes_remove", method="DELETE", path="/apis/{id}/scopes/{key}", path_params=("id", "key"), scope="apis:write", danger="destructive", approval=True, body=False, pagination=None), - "apis.update": OperationSpec(action=None, operation_id=None, method="PATCH", path="/apis/{id}", path_params=("id",), scope="apis:write", danger=None, approval=True, body=True, pagination=None), + "apis.update": OperationSpec(action="apis.update", operation_id=None, method="PATCH", path="/apis/{id}", path_params=("id",), scope="apis:write", danger="write", approval=True, body=True, pagination=None), "approvals.deny": OperationSpec(action="approvals.deny", operation_id="approvals_deny", method="POST", path="/agent-requests/{request_id}/deny", path_params=("request_id",), scope="approvals:write", danger="destructive", approval=True, body=False, pagination=None), "approvals.list": OperationSpec(action="approvals.list", operation_id="approvals_list", method="GET", path="/agent-requests", path_params=(), scope="approvals:read", danger="read", approval=True, body=False, pagination="cursor"), - "apps.blueprint": OperationSpec(action="apps.blueprint", operation_id="apps_blueprint", method="GET", path="/apps/{id}/blueprint", path_params=("id",), scope="apps:read", danger=None, approval=True, body=False, pagination=None), + "apps.blueprint": OperationSpec(action="apps.blueprint", operation_id="apps_blueprint", method="GET", path="/apps/{id}/blueprint", path_params=("id",), scope="apps:read", danger="read", approval=True, body=False, pagination=None), "apps.copy": OperationSpec(action="apps.copy", operation_id="apps_copy", method="POST", path="/apps/{id}/copy", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), "apps.create": OperationSpec(action="apps.create", operation_id="apps_create", method="POST", path="/apps", path_params=(), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), "apps.delete": OperationSpec(action="apps.delete", operation_id="apps_delete", method="DELETE", path="/apps/{id}", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=False, pagination=None), "apps.get": OperationSpec(action="apps.get", operation_id="apps_get", method="GET", path="/apps/{id}", path_params=("id",), scope="apps:read", danger="read", approval=True, body=False, pagination=None), - "apps.list": OperationSpec(action="apps.list", operation_id="apps_list", method="GET", path="/apps", path_params=(), scope="apps:read", danger=None, approval=True, body=False, pagination="cursor"), + "apps.list": OperationSpec(action="apps.list", operation_id="apps_list", method="GET", path="/apps", path_params=(), scope="apps:read", danger="read", approval=True, body=False, pagination="cursor"), "apps.manifest.set": OperationSpec(action="apps.manifest.set", operation_id="apps_manifest_set", method="PUT", path="/apps/{id}/manifest", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), "apps.manifest.sync": OperationSpec(action="apps.manifest.sync", operation_id="apps_manifest_sync", method="POST", path="/apps/{id}/manifest/sync", path_params=("id",), scope="apps:write", danger="write", approval=True, body=False, pagination=None), "apps.scopes.set": OperationSpec(action="apps.scopes.set", operation_id="apps_scopes_set", method="PUT", path="/apps/{id}/scopes", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), - "apps.secrets.list": OperationSpec(action="apps.secrets.list", operation_id="apps_secrets_list", method="GET", path="/apps/{id}/secrets", path_params=("id",), scope="apps:read", danger=None, approval=True, body=False, pagination=None), + "apps.secrets.list": OperationSpec(action="apps.secrets.list", operation_id="apps_secrets_list", method="GET", path="/apps/{id}/secrets", path_params=("id",), scope="apps:read", danger="read", approval=True, body=False, pagination=None), "apps.secrets.revoke": OperationSpec(action="apps.secrets.revoke", operation_id="apps_secrets_revoke", method="DELETE", path="/apps/{id}/secrets/{secret_id}", path_params=("id", "secret_id"), scope="apps:write", danger="critical", approval=True, body=False, pagination=None), "apps.secrets.rotate": OperationSpec(action="apps.secrets.rotate", operation_id="apps_secrets_rotate", method="POST", path="/apps/{id}/secrets", path_params=("id",), scope="apps:write", danger="critical", approval=True, body=True, pagination=None), "apps.settings.api_key_prefix": OperationSpec(action="apps.settings.api_key_prefix", operation_id="apps_settings_api_key_prefix", method="PUT", path="/apps/{id}/settings/api-key-prefix", path_params=("id",), scope="apps:write", danger="write", approval=True, body=True, pagination=None), @@ -3126,10 +3126,10 @@ class WebhooksUpdateBody(TypedDict): "hooks.get": OperationSpec(action="hooks.get", operation_id="hooks_get", method="GET", path="/hooks/{id}", path_params=("id",), scope="hooks:read", danger="read", approval=True, body=False, pagination=None), "hooks.list": OperationSpec(action="hooks.list", operation_id="hooks_list", method="GET", path="/hooks", path_params=(), scope="hooks:read", danger="read", approval=True, body=False, pagination="cursor"), "hooks.update": OperationSpec(action="hooks.update", operation_id="hooks_update", method="PATCH", path="/hooks/{id}", path_params=("id",), scope="hooks:write", danger="critical", approval=True, body=True, pagination=None), - "invitations.list": OperationSpec(action="invitations.list", operation_id="invitations_list", method="GET", path="/organizations/{organization_id}/invitations", path_params=("organization_id",), scope="invitations:read", danger=None, approval=True, body=False, pagination="cursor"), - "invitations.resend": OperationSpec(action="invitations.resend", operation_id="invitations_resend", method="POST", path="/organizations/{organization_id}/invitations/{invitation_id}/resend", path_params=("organization_id", "invitation_id"), scope="invitations:write", danger=None, approval=True, body=False, pagination=None), - "invitations.revoke": OperationSpec(action="invitations.revoke", operation_id="invitations_revoke", method="DELETE", path="/organizations/{organization_id}/invitations/{invitation_id}", path_params=("organization_id", "invitation_id"), scope="invitations:write", danger=None, approval=True, body=False, pagination=None), - "invitations.send": OperationSpec(action="invitations.send", operation_id="invitations_send", method="POST", path="/organizations/{organization_id}/invitations", path_params=("organization_id",), scope="invitations:write", danger=None, approval=True, body=True, pagination=None), + "invitations.list": OperationSpec(action="invitations.list", operation_id="invitations_list", method="GET", path="/organizations/{organization_id}/invitations", path_params=("organization_id",), scope="invitations:read", danger="read", approval=True, body=False, pagination="cursor"), + "invitations.resend": OperationSpec(action="invitations.resend", operation_id="invitations_resend", method="POST", path="/organizations/{organization_id}/invitations/{invitation_id}/resend", path_params=("organization_id", "invitation_id"), scope="invitations:write", danger="write", approval=True, body=False, pagination=None), + "invitations.revoke": OperationSpec(action="invitations.revoke", operation_id="invitations_revoke", method="DELETE", path="/organizations/{organization_id}/invitations/{invitation_id}", path_params=("organization_id", "invitation_id"), scope="invitations:write", danger="destructive", approval=True, body=False, pagination=None), + "invitations.send": OperationSpec(action="invitations.send", operation_id="invitations_send", method="POST", path="/organizations/{organization_id}/invitations", path_params=("organization_id",), scope="invitations:write", danger="write", approval=True, body=True, pagination=None), "keys.create": OperationSpec(action="keys.create", operation_id="keys_create", method="POST", path="/keys", path_params=(), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), "keys.list": OperationSpec(action="keys.list", operation_id="keys_list", method="GET", path="/keys", path_params=(), scope="keys:read", danger="read", approval=True, body=False, pagination="cursor"), "keys.revoke": OperationSpec(action="keys.revoke", operation_id="keys_revoke", method="DELETE", path="/keys/{id}", path_params=("id",), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), @@ -3144,29 +3144,29 @@ class WebhooksUpdateBody(TypedDict): "log_streams.list": OperationSpec(action="log_streams.list", operation_id="log_streams_list", method="GET", path="/log-streams", path_params=(), scope="log_streams:read", danger="read", approval=True, body=False, pagination="cursor"), "log_streams.test": OperationSpec(action="log_streams.test", operation_id="log_streams_test", method="POST", path="/log-streams/{id}/test", path_params=("id",), scope="log_streams:write", danger="write", approval=True, body=False, pagination=None), "log_streams.update": OperationSpec(action="log_streams.update", operation_id="log_streams_update", method="PATCH", path="/log-streams/{id}", path_params=("id",), scope="log_streams:write", danger="critical", approval=True, body=True, pagination=None), - "members.add": OperationSpec(action="members.add", operation_id="members_add", method="POST", path="/organizations/{organization_id}/members", path_params=("organization_id",), scope="members:write", danger=None, approval=True, body=True, pagination=None), - "members.list": OperationSpec(action="members.list", operation_id="members_list", method="GET", path="/organizations/{organization_id}/members", path_params=("organization_id",), scope="members:read", danger=None, approval=True, body=False, pagination="cursor"), - "members.remove": OperationSpec(action="members.remove", operation_id="members_remove", method="DELETE", path="/organizations/{organization_id}/members/{user_id}", path_params=("organization_id", "user_id"), scope="members:write", danger=None, approval=True, body=False, pagination=None), - "members.roles.grant": OperationSpec(action="members.roles.grant", operation_id="members_roles_grant", method="PUT", path="/organizations/{organization_id}/members/{user_id}/roles/{role_id}", path_params=("organization_id", "user_id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), - "members.roles.list": OperationSpec(action="members.roles.list", operation_id="members_roles_list", method="GET", path="/organizations/{organization_id}/members/{user_id}/roles", path_params=("organization_id", "user_id"), scope="roles:read", danger=None, approval=True, body=False, pagination=None), - "members.roles.revoke": OperationSpec(action="members.roles.revoke", operation_id="members_roles_revoke", method="DELETE", path="/organizations/{organization_id}/members/{user_id}/roles/{role_id}", path_params=("organization_id", "user_id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), - "members.update": OperationSpec(action="members.update", operation_id="members_update", method="PATCH", path="/organizations/{organization_id}/members/{user_id}", path_params=("organization_id", "user_id"), scope="members:write", danger=None, approval=True, body=True, pagination=None), - "organizations.create": OperationSpec(action="organizations.create", operation_id="organizations_create", method="POST", path="/organizations", path_params=(), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), - "organizations.delete": OperationSpec(action="organizations.delete", operation_id="organizations_delete", method="DELETE", path="/organizations/{id}", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=False, pagination=None), + "members.add": OperationSpec(action="members.add", operation_id="members_add", method="POST", path="/organizations/{organization_id}/members", path_params=("organization_id",), scope="members:write", danger="write", approval=True, body=True, pagination=None), + "members.list": OperationSpec(action="members.list", operation_id="members_list", method="GET", path="/organizations/{organization_id}/members", path_params=("organization_id",), scope="members:read", danger="read", approval=True, body=False, pagination="cursor"), + "members.remove": OperationSpec(action="members.remove", operation_id="members_remove", method="DELETE", path="/organizations/{organization_id}/members/{user_id}", path_params=("organization_id", "user_id"), scope="members:write", danger="destructive", approval=True, body=False, pagination=None), + "members.roles.grant": OperationSpec(action="members.roles.grant", operation_id="members_roles_grant", method="PUT", path="/organizations/{organization_id}/members/{user_id}/roles/{role_id}", path_params=("organization_id", "user_id", "role_id"), scope="roles:write", danger="write", approval=True, body=False, pagination=None), + "members.roles.list": OperationSpec(action="members.roles.list", operation_id="members_roles_list", method="GET", path="/organizations/{organization_id}/members/{user_id}/roles", path_params=("organization_id", "user_id"), scope="roles:read", danger="read", approval=True, body=False, pagination=None), + "members.roles.revoke": OperationSpec(action="members.roles.revoke", operation_id="members_roles_revoke", method="DELETE", path="/organizations/{organization_id}/members/{user_id}/roles/{role_id}", path_params=("organization_id", "user_id", "role_id"), scope="roles:write", danger="destructive", approval=True, body=False, pagination=None), + "members.update": OperationSpec(action="members.update", operation_id="members_update", method="PATCH", path="/organizations/{organization_id}/members/{user_id}", path_params=("organization_id", "user_id"), scope="members:write", danger="write", approval=True, body=True, pagination=None), + "organizations.create": OperationSpec(action="organizations.create", operation_id="organizations_create", method="POST", path="/organizations", path_params=(), scope="organizations:write", danger="write", approval=True, body=True, pagination=None), + "organizations.delete": OperationSpec(action="organizations.delete", operation_id="organizations_delete", method="DELETE", path="/organizations/{id}", path_params=("id",), scope="organizations:write", danger="destructive", approval=True, body=False, pagination=None), "organizations.domains.add": OperationSpec(action="organizations.domains.add", operation_id="organizations_domains_add", method="POST", path="/organizations/{organization_id}/domains", path_params=("organization_id",), scope="organizations:write", danger="write", approval=True, body=True, pagination=None), "organizations.domains.capture": OperationSpec(action="organizations.domains.capture", operation_id="organizations_domains_capture", method="PUT", path="/organizations/{organization_id}/domains/{domain_id}/capture", path_params=("organization_id", "domain_id"), scope="organizations:write", danger="critical", approval=True, body=True, pagination=None), - "organizations.domains.list": OperationSpec(action="organizations.domains.list", operation_id="organizations_domains_list", method="GET", path="/organizations/{organization_id}/domains", path_params=("organization_id",), scope="organizations:read", danger=None, approval=True, body=False, pagination=None), + "organizations.domains.list": OperationSpec(action="organizations.domains.list", operation_id="organizations_domains_list", method="GET", path="/organizations/{organization_id}/domains", path_params=("organization_id",), scope="organizations:read", danger="read", approval=True, body=False, pagination=None), "organizations.domains.remove": OperationSpec(action="organizations.domains.remove", operation_id="organizations_domains_remove", method="DELETE", path="/organizations/{organization_id}/domains/{domain_id}", path_params=("organization_id", "domain_id"), scope="organizations:write", danger="destructive", approval=True, body=False, pagination=None), "organizations.domains.verify": OperationSpec(action="organizations.domains.verify", operation_id="organizations_domains_verify", method="POST", path="/organizations/{organization_id}/domains/{domain_id}/verify", path_params=("organization_id", "domain_id"), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), - "organizations.get": OperationSpec(action="organizations.get", operation_id="organizations_get", method="GET", path="/organizations/{id}", path_params=("id",), scope="organizations:read", danger=None, approval=True, body=False, pagination=None), - "organizations.list": OperationSpec(action="organizations.list", operation_id="organizations_list", method="GET", path="/organizations", path_params=(), scope="organizations:read", danger=None, approval=True, body=False, pagination="cursor"), + "organizations.get": OperationSpec(action="organizations.get", operation_id="organizations_get", method="GET", path="/organizations/{id}", path_params=("id",), scope="organizations:read", danger="read", approval=True, body=False, pagination=None), + "organizations.list": OperationSpec(action="organizations.list", operation_id="organizations_list", method="GET", path="/organizations", path_params=(), scope="organizations:read", danger="read", approval=True, body=False, pagination="cursor"), "organizations.portal_links.create": OperationSpec(action="organizations.portal_links.create", operation_id="organizations_portal_links_create", method="POST", path="/organizations/{organization_id}/portal-links", path_params=("organization_id",), scope="portal_links:write", danger="critical", approval=True, body=True, pagination=None), "organizations.portal_links.list": OperationSpec(action="organizations.portal_links.list", operation_id="organizations_portal_links_list", method="GET", path="/organizations/{organization_id}/portal-links", path_params=("organization_id",), scope="portal_links:read", danger="read", approval=True, body=False, pagination=None), "organizations.portal_links.revoke": OperationSpec(action="organizations.portal_links.revoke", operation_id="organizations_portal_links_revoke", method="DELETE", path="/organizations/{organization_id}/portal-links/{id}", path_params=("organization_id", "id"), scope="portal_links:write", danger="destructive", approval=True, body=False, pagination=None), "organizations.reactivate": OperationSpec(action="organizations.reactivate", operation_id="organizations_reactivate", method="POST", path="/organizations/{id}/reactivate", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), "organizations.suspend": OperationSpec(action="organizations.suspend", operation_id="organizations_suspend", method="POST", path="/organizations/{id}/suspend", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=False, pagination=None), - "organizations.transfer_ownership": OperationSpec(action="organizations.transfer_ownership", operation_id="organizations_transfer_ownership", method="POST", path="/organizations/{id}/transfer-ownership", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), - "organizations.update": OperationSpec(action="organizations.update", operation_id="organizations_update", method="PATCH", path="/organizations/{id}", path_params=("id",), scope="organizations:write", danger=None, approval=True, body=True, pagination=None), + "organizations.transfer_ownership": OperationSpec(action="organizations.transfer_ownership", operation_id="organizations_transfer_ownership", method="POST", path="/organizations/{id}/transfer-ownership", path_params=("id",), scope="organizations:write", danger="critical", approval=True, body=True, pagination=None), + "organizations.update": OperationSpec(action="organizations.update", operation_id="organizations_update", method="PATCH", path="/organizations/{id}", path_params=("id",), scope="organizations:write", danger="write", approval=True, body=True, pagination=None), "permissions.create": OperationSpec(action="permissions.create", operation_id="permissions_create", method="POST", path="/permissions", path_params=(), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), "permissions.delete": OperationSpec(action="permissions.delete", operation_id="permissions_delete", method="DELETE", path="/permissions/{id}", path_params=("id",), scope="role_definitions:write", danger="destructive", approval=True, body=False, pagination=None), "permissions.list": OperationSpec(action="permissions.list", operation_id="permissions_list", method="GET", path="/permissions", path_params=(), scope="roles:read", danger="read", approval=True, body=False, pagination="cursor"), @@ -3179,7 +3179,7 @@ class WebhooksUpdateBody(TypedDict): "roles.create": OperationSpec(action="roles.create", operation_id="roles_create", method="POST", path="/roles", path_params=(), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), "roles.delete": OperationSpec(action="roles.delete", operation_id="roles_delete", method="DELETE", path="/roles/{id}", path_params=("id",), scope="role_definitions:write", danger="destructive", approval=True, body=False, pagination=None), "roles.get": OperationSpec(action="roles.get", operation_id="roles_get", method="GET", path="/roles/{id}", path_params=("id",), scope="roles:read", danger="read", approval=True, body=False, pagination=None), - "roles.list": OperationSpec(action="roles.list", operation_id="roles_list", method="GET", path="/roles", path_params=(), scope="roles:read", danger=None, approval=True, body=False, pagination=None), + "roles.list": OperationSpec(action="roles.list", operation_id="roles_list", method="GET", path="/roles", path_params=(), scope="roles:read", danger="read", approval=True, body=False, pagination=None), "roles.permissions.grant": OperationSpec(action="roles.permissions.grant", operation_id="roles_permissions_grant", method="PUT", path="/roles/{id}/permissions/{permission_id}", path_params=("id", "permission_id"), scope="role_definitions:write", danger="write", approval=True, body=False, pagination=None), "roles.permissions.revoke": OperationSpec(action="roles.permissions.revoke", operation_id="roles_permissions_revoke", method="DELETE", path="/roles/{id}/permissions/{permission_id}", path_params=("id", "permission_id"), scope="role_definitions:write", danger="destructive", approval=True, body=False, pagination=None), "roles.update": OperationSpec(action="roles.update", operation_id="roles_update", method="PATCH", path="/roles/{id}", path_params=("id",), scope="role_definitions:write", danger="write", approval=True, body=True, pagination=None), @@ -3218,7 +3218,7 @@ class WebhooksUpdateBody(TypedDict): "sso.domains.verify": OperationSpec(action="sso.domains.verify", operation_id="sso_domains_verify", method="POST", path="/sso/domains/{id}/verify", path_params=("id",), scope="sso:write", danger="write", approval=True, body=True, pagination=None), "sso.saml_metadata.import": OperationSpec(action="sso.saml_metadata.import", operation_id="sso_saml_metadata_import", method="POST", path="/sso/saml-metadata", path_params=(), scope="sso:write", danger="write", approval=True, body=True, pagination=None), "support_sessions.end": OperationSpec(action="support_sessions.end", operation_id="support_sessions_end", method="DELETE", path="/support-sessions/{id}", path_params=("id",), scope="support:write", danger="destructive", approval=True, body=False, pagination=None), - "support_sessions.start": OperationSpec(action="support_sessions.start", operation_id="support_sessions_start", method="POST", path="/support-sessions", path_params=(), scope="support:write", danger=None, approval=True, body=True, pagination=None), + "support_sessions.start": OperationSpec(action="support_sessions.start", operation_id="support_sessions_start", method="POST", path="/support-sessions", path_params=(), scope="support:write", danger="critical", approval=True, body=True, pagination=None), "token_vault.grants.create": OperationSpec(action="token_vault.grants.create", operation_id="token_vault_grants_create", method="POST", path="/token-vault/secrets/{id}/grants", path_params=("id",), scope="token_vault:write", danger="critical", approval=True, body=True, pagination=None), "token_vault.grants.delete": OperationSpec(action="token_vault.grants.delete", operation_id="token_vault_grants_delete", method="DELETE", path="/token-vault/secrets/{id}/grants/{client_id}", path_params=("id", "client_id"), scope="token_vault:write", danger="destructive", approval=True, body=True, pagination=None), "token_vault.secrets.create": OperationSpec(action="token_vault.secrets.create", operation_id="token_vault_secrets_create", method="POST", path="/token-vault/secrets", path_params=(), scope="token_vault:write", danger="write", approval=True, body=True, pagination=None), @@ -3226,20 +3226,20 @@ class WebhooksUpdateBody(TypedDict): "token_vault.secrets.list": OperationSpec(action="token_vault.secrets.list", operation_id="token_vault_secrets_list", method="GET", path="/token-vault/secrets", path_params=(), scope="token_vault:read", danger="read", approval=True, body=False, pagination="cursor"), "token_vault.secrets.revoke": OperationSpec(action="token_vault.secrets.revoke", operation_id="token_vault_secrets_revoke", method="POST", path="/token-vault/secrets/{id}/revoke", path_params=("id",), scope="token_vault:write", danger="destructive", approval=True, body=True, pagination=None), "token_vault.secrets.rotate": OperationSpec(action="token_vault.secrets.rotate", operation_id="token_vault_secrets_rotate", method="POST", path="/token-vault/secrets/{id}/rotate", path_params=("id",), scope="token_vault:write", danger="critical", approval=True, body=True, pagination=None), - "users.create": OperationSpec(action="users.create", operation_id="users_create", method="POST", path="/users", path_params=(), scope="users:write", danger=None, approval=True, body=True, pagination=None), - "users.deactivate": OperationSpec(action="users.deactivate", operation_id="users_deactivate", method="DELETE", path="/users/{id}", path_params=("id",), scope="users:write", danger=None, approval=True, body=False, pagination=None), - "users.environment_roles.get": OperationSpec(action="users.environment_roles.get", operation_id="users_environment_roles_get", method="GET", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:read", danger=None, approval=True, body=False, pagination=None), - "users.environment_roles.grant": OperationSpec(action="users.environment_roles.grant", operation_id="users_environment_roles_grant", method="PUT", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), - "users.environment_roles.list": OperationSpec(action="users.environment_roles.list", operation_id="users_environment_roles_list", method="GET", path="/users/{id}/environment-roles", path_params=("id",), scope="roles:read", danger=None, approval=True, body=False, pagination=None), - "users.environment_roles.revoke": OperationSpec(action="users.environment_roles.revoke", operation_id="users_environment_roles_revoke", method="DELETE", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:write", danger=None, approval=True, body=False, pagination=None), + "users.create": OperationSpec(action="users.create", operation_id="users_create", method="POST", path="/users", path_params=(), scope="users:write", danger="write", approval=True, body=True, pagination=None), + "users.deactivate": OperationSpec(action="users.deactivate", operation_id="users_deactivate", method="DELETE", path="/users/{id}", path_params=("id",), scope="users:write", danger="destructive", approval=True, body=False, pagination=None), + "users.environment_roles.get": OperationSpec(action="users.environment_roles.get", operation_id="users_environment_roles_get", method="GET", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:read", danger="read", approval=True, body=False, pagination=None), + "users.environment_roles.grant": OperationSpec(action="users.environment_roles.grant", operation_id="users_environment_roles_grant", method="PUT", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:write", danger="critical", approval=True, body=False, pagination=None), + "users.environment_roles.list": OperationSpec(action="users.environment_roles.list", operation_id="users_environment_roles_list", method="GET", path="/users/{id}/environment-roles", path_params=("id",), scope="roles:read", danger="read", approval=True, body=False, pagination=None), + "users.environment_roles.revoke": OperationSpec(action="users.environment_roles.revoke", operation_id="users_environment_roles_revoke", method="DELETE", path="/users/{id}/environment-roles/{role_id}", path_params=("id", "role_id"), scope="roles:write", danger="destructive", approval=True, body=False, pagination=None), "users.erase": OperationSpec(action="users.erase", operation_id="users_erase", method="POST", path="/users/{id}/erase", path_params=("id",), scope="users:erase", danger="critical", approval=True, body=False, pagination=None), - "users.get": OperationSpec(action="users.get", operation_id="users_get", method="GET", path="/users/{id}", path_params=("id",), scope="users:read", danger=None, approval=True, body=False, pagination=None), - "users.list": OperationSpec(action="users.list", operation_id="users_list", method="GET", path="/users", path_params=(), scope="users:read", danger=None, approval=True, body=False, pagination="cursor"), + "users.get": OperationSpec(action="users.get", operation_id="users_get", method="GET", path="/users/{id}", path_params=("id",), scope="users:read", danger="read", approval=True, body=False, pagination=None), + "users.list": OperationSpec(action="users.list", operation_id="users_list", method="GET", path="/users", path_params=(), scope="users:read", danger="read", approval=True, body=False, pagination="cursor"), "users.mfa.reset": OperationSpec(action="users.mfa.reset", operation_id="users_mfa_reset", method="DELETE", path="/users/{id}/mfa", path_params=("id",), scope="users:write", danger="critical", approval=True, body=False, pagination=None), "users.password.set": OperationSpec(action="users.password.set", operation_id="users_password_set", method="POST", path="/users/{id}/password", path_params=("id",), scope="users:write", danger="critical", approval=True, body=True, pagination=None), "users.password_reset.send": OperationSpec(action="users.password_reset.send", operation_id="users_password_reset_send", method="POST", path="/users/{id}/password-reset", path_params=("id",), scope="users:write", danger="write", approval=True, body=False, pagination=None), "users.reactivate": OperationSpec(action="users.reactivate", operation_id="users_reactivate", method="POST", path="/users/{id}/reactivate", path_params=("id",), scope="users:write", danger="write", approval=True, body=False, pagination=None), - "users.sessions.list": OperationSpec(action="users.sessions.list", operation_id="users_sessions_list", method="GET", path="/users/{id}/sessions", path_params=("id",), scope="users:read", danger=None, approval=True, body=False, pagination=None), + "users.sessions.list": OperationSpec(action="users.sessions.list", operation_id="users_sessions_list", method="GET", path="/users/{id}/sessions", path_params=("id",), scope="users:read", danger="read", approval=True, body=False, pagination=None), "users.sessions.revoke": OperationSpec(action="users.sessions.revoke", operation_id="users_sessions_revoke", method="DELETE", path="/users/{id}/sessions/{session_id}", path_params=("id", "session_id"), scope="users:write", danger="destructive", approval=True, body=False, pagination=None), "users.sessions.revoke_all": OperationSpec(action="users.sessions.revoke_all", operation_id="users_sessions_revoke_all", method="DELETE", path="/users/{id}/sessions", path_params=("id",), scope="users:write", danger="destructive", approval=True, body=False, pagination=None), "users.update": OperationSpec(action="users.update", operation_id="users_update", method="PATCH", path="/users/{id}", path_params=("id",), scope="users:write", danger="critical", approval=True, body=True, pagination=None), @@ -3421,7 +3421,7 @@ def list(self, organization_id: str, query: ApiKeysListQuery | None = None, *, a `status`. Never the key itself. Narrow to one app with `?client_id=`. ``GET /organizations/{organization_id}/api-keys`` · action ``api_keys.list`` - Scope ``api_keys:read``. + Scope ``api_keys:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["api_keys.list"], (organization_id,), query, approval=approval, approval_id=approval_id, headers=headers) @@ -3444,7 +3444,7 @@ def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s management key as the actor. ``DELETE /api-keys/{id}`` · action ``api_keys.revoke`` - Scope ``api_keys:write``. + Scope ``api_keys:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["api_keys.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -3514,8 +3514,8 @@ def create(self, body: ApisCreateBody, *, approval: ApprovalMode = "wait", idemp `organization_id` makes the API one organization's; left out, the environment owns it. Refusals are `422 invalid_api` with the reason. - ``POST /apis`` - Scope ``apis:write``. + ``POST /apis`` · action ``apis.create`` + Scope ``apis:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apis.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -3532,8 +3532,8 @@ def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s Requires scope `apis:write`. Tokens already minted for it keep their `aud` until they expire; apps holding its scope keys keep them as plain scopes. - ``DELETE /apis/{id}`` - Scope ``apis:write``. + ``DELETE /apis/{id}`` · action ``apis.delete`` + Scope ``apis:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apis.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -3549,8 +3549,8 @@ def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | No Requires scope `apis:read`. - ``GET /apis/{id}`` - Scope ``apis:read``. + ``GET /apis/{id}`` · action ``apis.get`` + Scope ``apis:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apis.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -3566,8 +3566,8 @@ def list(self, query: ApisListQuery | None = None, *, approval: ApprovalMode = " Requires scope `apis:read`. - ``GET /apis`` - Scope ``apis:read``. + ``GET /apis`` · action ``apis.list`` + Scope ``apis:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apis.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) @@ -3590,8 +3590,8 @@ def update(self, id: str, body: ApisUpdateBody | None = None, *, approval: Appro updated, scopes left out are removed. All or nothing. The identifier never changes: register a new API instead. - ``PATCH /apis/{id}`` - Scope ``apis:write``. + ``PATCH /apis/{id}`` · action ``apis.update`` + Scope ``apis:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apis.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -3728,7 +3728,7 @@ def list(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | N through its overlap. An app holds a handful at most, so there is no page to turn. ``GET /apps/{id}/secrets`` · action ``apps.secrets.list`` - Scope ``apps:read``. + Scope ``apps:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apps.secrets.list"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -3868,7 +3868,7 @@ def blueprint(self, id: str, *, approval: ApprovalMode = "wait", approval_id: st creates the same app in another environment. ``GET /apps/{id}/blueprint`` · action ``apps.blueprint`` - Scope ``apps:read``. + Scope ``apps:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apps.blueprint"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -3969,7 +3969,7 @@ def list(self, query: AppsListQuery | None = None, *, approval: ApprovalMode = " Requires scope `apps:read`. Never a secret. ``GET /apps`` · action ``apps.list`` - Scope ``apps:read``. + Scope ``apps:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["apps.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) @@ -4871,7 +4871,7 @@ def list(self, organization_id: str, query: InvitationsListQuery | None = None, Requires scope `invitations:read`. Pending and unexpired only. ``GET /organizations/{organization_id}/invitations`` · action ``invitations.list`` - Scope ``invitations:read``. + Scope ``invitations:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["invitations.list"], (organization_id,), query, approval=approval, approval_id=approval_id, headers=headers) @@ -4898,7 +4898,7 @@ def resend(self, organization_id: str, invitation_id: str, *, approval: Approval was accepted, withdrawn or has expired; `503 mail_failed` keeps the invitation. ``POST /organizations/{organization_id}/invitations/{invitation_id}/resend`` · action ``invitations.resend`` - Scope ``invitations:write``. + Scope ``invitations:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["invitations.resend"], (organization_id, invitation_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -4916,7 +4916,7 @@ def revoke(self, organization_id: str, invitation_id: str, *, approval: Approval for it go with it. `409 not_pending` for one already accepted, withdrawn or expired. ``DELETE /organizations/{organization_id}/invitations/{invitation_id}`` · action ``invitations.revoke`` - Scope ``invitations:write``. + Scope ``invitations:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["invitations.revoke"], (organization_id, invitation_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -4951,7 +4951,7 @@ def send(self, organization_id: str, body: InvitationsSendBody, *, approval: App `503 mail_failed` (nothing was created — retry). ``POST /organizations/{organization_id}/invitations`` · action ``invitations.send`` - Scope ``invitations:write``. + Scope ``invitations:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["invitations.send"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5252,7 +5252,7 @@ def grant(self, organization_id: str, user_id: str, role_id: str, query: Members this organization cannot use), `409 role_conflict` (segregation of duties). ``PUT /organizations/{organization_id}/members/{user_id}/roles/{role_id}`` · action ``members.roles.grant`` - Scope ``roles:write``. + Scope ``roles:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.roles.grant"], (organization_id, user_id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5271,7 +5271,7 @@ def list(self, organization_id: str, user_id: str, *, approval: ApprovalMode = " `/users/{id}/environment-roles`. ``GET /organizations/{organization_id}/members/{user_id}/roles`` · action ``members.roles.list`` - Scope ``roles:read``. + Scope ``roles:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.roles.list"], (organization_id, user_id), None, approval=approval, approval_id=approval_id, headers=headers) @@ -5288,7 +5288,7 @@ def revoke(self, organization_id: str, user_id: str, role_id: str, query: Member Requires scope `roles:write`. Idempotent — a role the member does not hold is a 204 too. ``DELETE /organizations/{organization_id}/members/{user_id}/roles/{role_id}`` · action ``members.roles.revoke`` - Scope ``roles:write``. + Scope ``roles:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.roles.revoke"], (organization_id, user_id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5327,7 +5327,7 @@ def add(self, organization_id: str, body: MembersAddBody | None = None, *, appro the role with `PATCH`. ``POST /organizations/{organization_id}/members`` · action ``members.add`` - Scope ``members:write``. + Scope ``members:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.add"], (organization_id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5344,7 +5344,7 @@ def list(self, organization_id: str, query: MembersListQuery | None = None, *, a Requires scope `members:read`. Every membership, whatever its status, oldest first. ``GET /organizations/{organization_id}/members`` · action ``members.list`` - Scope ``members:read``. + Scope ``members:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.list"], (organization_id,), query, approval=approval, approval_id=approval_id, headers=headers) @@ -5366,7 +5366,7 @@ def remove(self, organization_id: str, user_id: str, *, approval: ApprovalMode = membership. Removing the only owner is refused with `409 last_owner`. ``DELETE /organizations/{organization_id}/members/{user_id}`` · action ``members.remove`` - Scope ``members:write``. + Scope ``members:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.remove"], (organization_id, user_id), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5384,7 +5384,7 @@ def update(self, organization_id: str, user_id: str, body: MembersUpdateBody, *, owner is refused with `409 last_owner` — transfer ownership first. ``PATCH /organizations/{organization_id}/members/{user_id}`` · action ``members.update`` - Scope ``members:write``. + Scope ``members:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["members.update"], (organization_id, user_id), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5443,7 +5443,7 @@ def list(self, organization_id: str, *, approval: ApprovalMode = "wait", approva that proves it (`record_name`, `record_value`). A handful at most; not paged. ``GET /organizations/{organization_id}/domains`` · action ``organizations.domains.list`` - Scope ``organizations:read``. + Scope ``organizations:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.domains.list"], (organization_id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -5565,7 +5565,7 @@ def create(self, body: OrganizationsCreateBody, *, approval: ApprovalMode = "wai Requires scope `organizations:write`. ``POST /organizations`` · action ``organizations.create`` - Scope ``organizations:write``. + Scope ``organizations:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5588,7 +5588,7 @@ def delete(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s projects on this platform (a platform customer, not a tenant of your app). ``DELETE /organizations/{id}`` · action ``organizations.delete`` - Scope ``organizations:write``. + Scope ``organizations:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.delete"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5605,7 +5605,7 @@ def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | No Requires scope `organizations:read`. ``GET /organizations/{id}`` · action ``organizations.get`` - Scope ``organizations:read``. + Scope ``organizations:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -5623,7 +5623,7 @@ def list(self, query: OrganizationsListQuery | None = None, *, approval: Approva with `q` (a fragment of the name or slug) or `status`. ``GET /organizations`` · action ``organizations.list`` - Scope ``organizations:read``. + Scope ``organizations:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) @@ -5684,7 +5684,7 @@ def transfer_ownership(self, id: str, body: OrganizationsTransferOwnershipBody, Refusals: `422 not_a_member`, `409 not_active`, `409 already_owner`. ``POST /organizations/{id}/transfer-ownership`` · action ``organizations.transfer_ownership`` - Scope ``organizations:write``. + Scope ``organizations:write`` · danger: critical. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.transfer_ownership"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -5704,7 +5704,7 @@ def update(self, id: str, body: OrganizationsUpdateBody | None = None, *, approv organization's trail as `organization.renamed`, with the values before and after. ``PATCH /organizations/{id}`` · action ``organizations.update`` - Scope ``organizations:write``. + Scope ``organizations:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["organizations.update"], (id,), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -6014,7 +6014,7 @@ def list(self, query: RolesListQuery | None = None, *, approval: ApprovalMode = app's roles; `?organization_id=` to what can be granted in that organization. ``GET /roles`` · action ``roles.list`` - Scope ``roles:read``. + Scope ``roles:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["roles.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) @@ -6784,7 +6784,7 @@ def start(self, body: SupportSessionsStartBody, *, approval: ApprovalMode = "wai one token and are refused before the session starts (`422 invalid_target`). ``POST /support-sessions`` · action ``support_sessions.start`` - Scope ``support:write``. + Scope ``support:write`` · danger: critical. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["support_sessions.start"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -6954,7 +6954,7 @@ def get(self, id: str, role_id: str, query: UsersEnvironmentRolesGetQuery | None Requires scope `roles:read`. `200` with the grant, or `404` when it is not held. ``GET /users/{id}/environment-roles/{role_id}`` · action ``users.environment_roles.get`` - Scope ``roles:read``. + Scope ``roles:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.get"], (id, role_id), query, approval=approval, approval_id=approval_id, headers=headers) @@ -6976,7 +6976,7 @@ def grant(self, id: str, role_id: str, query: UsersEnvironmentRolesGrantQuery | person belongs to with `409 role_conflict`. ``PUT /users/{id}/environment-roles/{role_id}`` · action ``users.environment_roles.grant`` - Scope ``roles:write``. + Scope ``roles:write`` · danger: critical. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.grant"], (id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -6993,7 +6993,7 @@ def list(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | N Requires scope `roles:read`. ``GET /users/{id}/environment-roles`` · action ``users.environment_roles.list`` - Scope ``roles:read``. + Scope ``roles:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.list"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -7010,7 +7010,7 @@ def revoke(self, id: str, role_id: str, query: UsersEnvironmentRolesRevokeQuery Requires scope `roles:write`. Idempotent. ``DELETE /users/{id}/environment-roles/{role_id}`` · action ``users.environment_roles.revoke`` - Scope ``roles:write``. + Scope ``roles:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.environment_roles.revoke"], (id, role_id), query, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -7108,7 +7108,7 @@ def list(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | N `impersonation` marks a session somebody else opened as this person. ``GET /users/{id}/sessions`` · action ``users.sessions.list`` - Scope ``users:read``. + Scope ``users:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.sessions.list"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -7196,7 +7196,7 @@ def create(self, body: UsersCreateBody, *, approval: ApprovalMode = "wait", idem Requires scope `users:write`. Password is optional — omit it to create a passwordless identity the user completes via an invite/magic-link. ``POST /users`` · action ``users.create`` - Scope ``users:write``. + Scope ``users:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -7216,7 +7216,7 @@ def deactivate(self, id: str, *, approval: ApprovalMode = "wait", idempotency_ke `disabled` state. Idempotent. The console's "Deactivate" is this action. ``DELETE /users/{id}`` · action ``users.deactivate`` - Scope ``users:write``. + Scope ``users:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.deactivate"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -7250,7 +7250,7 @@ def get(self, id: str, *, approval: ApprovalMode = "wait", approval_id: str | No Requires scope `users:read`. ``GET /users/{id}`` · action ``users.get`` - Scope ``users:read``. + Scope ``users:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.get"], (id,), None, approval=approval, approval_id=approval_id, headers=headers) @@ -7269,7 +7269,7 @@ def list(self, query: UsersListQuery | None = None, *, approval: ApprovalMode = of the address or the name) and `status`. Filters combine. ``GET /users`` · action ``users.list`` - Scope ``users:read``. + Scope ``users:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(ENVIRONMENT_OPERATIONS["users.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) diff --git a/src/cbox_id/management/generated/workspace.py b/src/cbox_id/management/generated/workspace.py index a80b055..55b2d2b 100644 --- a/src/cbox_id/management/generated/workspace.py +++ b/src/cbox_id/management/generated/workspace.py @@ -295,28 +295,28 @@ class WorkspaceSettingsUpdateBody(TypedDict): "environments.domain.remove": OperationSpec(action="environments.domain.remove", operation_id="environments_domain_remove", method="DELETE", path="/workspace/environments/{environment_id}/domain", path_params=("environment_id",), scope="environments:write", danger="destructive", approval=True, body=False, pagination=None), "environments.domain.request": OperationSpec(action="environments.domain.request", operation_id="environments_domain_request", method="POST", path="/workspace/environments/{environment_id}/domain", path_params=("environment_id",), scope="environments:write", danger="write", approval=True, body=True, pagination=None), "environments.domain.verify": OperationSpec(action="environments.domain.verify", operation_id="environments_domain_verify", method="POST", path="/workspace/environments/{environment_id}/domain/verify", path_params=("environment_id",), scope="environments:write", danger="write", approval=True, body=False, pagination=None), - "environments.list": OperationSpec(action="environments.list", operation_id="environments_list", method="GET", path="/workspace/environments", path_params=(), scope="workspace:read", danger=None, approval=True, body=False, pagination="page"), + "environments.list": OperationSpec(action="environments.list", operation_id="environments_list", method="GET", path="/workspace/environments", path_params=(), scope="workspace:read", danger="read", approval=True, body=False, pagination="page"), "keys.environment.create": OperationSpec(action="keys.environment.create", operation_id="keys_environment_create", method="POST", path="/workspace/environments/{environment_id}/keys", path_params=("environment_id",), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), "keys.environment.revoke": OperationSpec(action="keys.environment.revoke", operation_id="keys_environment_revoke", method="DELETE", path="/workspace/environments/{environment_id}/keys/{id}", path_params=("environment_id", "id"), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), "keys.workspace.create": OperationSpec(action="keys.workspace.create", operation_id="keys_workspace_create", method="POST", path="/workspace/keys", path_params=(), scope="keys:write", danger="critical", approval=True, body=True, pagination=None), "keys.workspace.list": OperationSpec(action="keys.workspace.list", operation_id="keys_workspace_list", method="GET", path="/workspace/keys", path_params=(), scope="workspace:read", danger="read", approval=True, body=False, pagination="page"), "keys.workspace.revoke": OperationSpec(action="keys.workspace.revoke", operation_id="keys_workspace_revoke", method="DELETE", path="/workspace/keys/{id}", path_params=("id",), scope="keys:write", danger="destructive", approval=True, body=False, pagination=None), - "projects.create": OperationSpec(action="projects.create", operation_id="projects_create", method="POST", path="/workspace/projects", path_params=(), scope="projects:write", danger=None, approval=True, body=True, pagination=None), - "projects.list": OperationSpec(action="projects.list", operation_id="projects_list", method="GET", path="/workspace/projects", path_params=(), scope="workspace:read", danger=None, approval=True, body=False, pagination=None), + "projects.create": OperationSpec(action="projects.create", operation_id="projects_create", method="POST", path="/workspace/projects", path_params=(), scope="projects:write", danger="write", approval=True, body=True, pagination=None), + "projects.list": OperationSpec(action="projects.list", operation_id="projects_list", method="GET", path="/workspace/projects", path_params=(), scope="workspace:read", danger="read", approval=True, body=False, pagination=None), "projects.reactivate": OperationSpec(action="projects.reactivate", operation_id="projects_reactivate", method="POST", path="/workspace/projects/{id}/reactivate", path_params=("id",), scope="projects:write", danger="write", approval=True, body=False, pagination=None), "projects.rename": OperationSpec(action="projects.rename", operation_id="projects_rename", method="PATCH", path="/workspace/projects/{id}", path_params=("id",), scope="projects:write", danger="write", approval=True, body=True, pagination=None), "projects.suspend": OperationSpec(action="projects.suspend", operation_id="projects_suspend", method="POST", path="/workspace/projects/{id}/suspend", path_params=("id",), scope="projects:write", danger="write", approval=True, body=False, pagination=None), "projects.verification.resend": OperationSpec(action="projects.verification.resend", operation_id="projects_verification_resend", method="POST", path="/workspace/projects/verification/resend", path_params=(), scope="projects:write", danger="write", approval=True, body=False, pagination=None), "team.environment_access": OperationSpec(action="team.environment_access", operation_id="team_environment_access", method="PUT", path="/workspace/members/{id}/access", path_params=("id",), scope="team:write", danger="write", approval=True, body=True, pagination=None), - "team.invitations.list": OperationSpec(action="team.invitations.list", operation_id="team_invitations_list", method="GET", path="/workspace/invitations", path_params=(), scope="team:read", danger=None, approval=True, body=False, pagination=None), - "team.invitations.resend": OperationSpec(action="team.invitations.resend", operation_id="team_invitations_resend", method="POST", path="/workspace/invitations/{id}/resend", path_params=("id",), scope="team:write", danger=None, approval=True, body=False, pagination=None), - "team.invitations.revoke": OperationSpec(action="team.invitations.revoke", operation_id="team_invitations_revoke", method="DELETE", path="/workspace/invitations/{id}", path_params=("id",), scope="team:write", danger=None, approval=True, body=False, pagination=None), - "team.invite": OperationSpec(action="team.invite", operation_id="team_invite", method="POST", path="/workspace/members", path_params=(), scope="team:write", danger=None, approval=True, body=True, pagination=None), - "team.list": OperationSpec(action="team.list", operation_id="team_list", method="GET", path="/workspace/members", path_params=(), scope="team:read", danger=None, approval=True, body=False, pagination="page"), + "team.invitations.list": OperationSpec(action="team.invitations.list", operation_id="team_invitations_list", method="GET", path="/workspace/invitations", path_params=(), scope="team:read", danger="read", approval=True, body=False, pagination=None), + "team.invitations.resend": OperationSpec(action="team.invitations.resend", operation_id="team_invitations_resend", method="POST", path="/workspace/invitations/{id}/resend", path_params=("id",), scope="team:write", danger="write", approval=True, body=False, pagination=None), + "team.invitations.revoke": OperationSpec(action="team.invitations.revoke", operation_id="team_invitations_revoke", method="DELETE", path="/workspace/invitations/{id}", path_params=("id",), scope="team:write", danger="destructive", approval=True, body=False, pagination=None), + "team.invite": OperationSpec(action="team.invite", operation_id="team_invite", method="POST", path="/workspace/members", path_params=(), scope="team:write", danger="write", approval=True, body=True, pagination=None), + "team.list": OperationSpec(action="team.list", operation_id="team_list", method="GET", path="/workspace/members", path_params=(), scope="team:read", danger="read", approval=True, body=False, pagination="page"), "team.remove": OperationSpec(action="team.remove", operation_id="team_remove", method="DELETE", path="/workspace/members/{id}", path_params=("id",), scope="team:write", danger="destructive", approval=True, body=False, pagination=None), "team.role": OperationSpec(action="team.role", operation_id="team_role", method="PATCH", path="/workspace/members/{id}/role", path_params=("id",), scope="team:write", danger="write", approval=True, body=True, pagination=None), "team.transfer_ownership": OperationSpec(action="team.transfer_ownership", operation_id="team_transfer_ownership", method="POST", path="/workspace/members/{id}/transfer-ownership", path_params=("id",), scope="team:write", danger="critical", approval=True, body=False, pagination=None), - "workspace.get": OperationSpec(action="workspace.get", operation_id="workspace_get", method="GET", path="/workspace", path_params=(), scope="workspace:read", danger=None, approval=True, body=False, pagination=None), + "workspace.get": OperationSpec(action="workspace.get", operation_id="workspace_get", method="GET", path="/workspace", path_params=(), scope="workspace:read", danger="read", approval=True, body=False, pagination=None), "workspace.settings.update": OperationSpec(action="workspace.settings.update", operation_id="workspace_settings_update", method="PATCH", path="/workspace", path_params=(), scope="settings:write", danger="write", approval=True, body=True, pagination=None), } @@ -441,7 +441,7 @@ def list(self, query: EnvironmentsListQuery | None = None, *, approval: Approval Requires scope `workspace:read` (any role). ``GET /workspace/environments`` · action ``environments.list`` - Scope ``workspace:read``. + Scope ``workspace:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["environments.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) @@ -606,7 +606,7 @@ def create(self, body: ProjectsCreateBody, *, approval: ApprovalMode = "wait", i Stand up another independently-billed IdP product. Requires scope `projects:write` and the `manage-environments` capability (owner/admin/developer). ``POST /workspace/projects`` · action ``projects.create`` - Scope ``projects:write``. + Scope ``projects:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["projects.create"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -623,7 +623,7 @@ def list(self, *, approval: ApprovalMode = "wait", approval_id: str | None = Non Requires scope `workspace:read` (any role). The workspace's projects (IdP products). Each carries its own plan and environment allowance. ``GET /workspace/projects`` · action ``projects.list`` - Scope ``workspace:read``. + Scope ``workspace:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["projects.list"], (), None, approval=approval, approval_id=approval_id, headers=headers) @@ -698,7 +698,7 @@ def list(self, *, approval: ApprovalMode = "wait", approval_id: str | None = Non Requires scope `team:read` and the `read-members` capability. The team's invitations nobody has accepted yet, newest first (at most 100). ``GET /workspace/invitations`` · action ``team.invitations.list`` - Scope ``team:read``. + Scope ``team:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["team.invitations.list"], (), None, approval=approval, approval_id=approval_id, headers=headers) @@ -717,7 +717,7 @@ def resend(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s (`429`, `too_soon`). If the mail server refuses, the earlier invitation is kept (`503`). ``POST /workspace/invitations/{id}/resend`` · action ``team.invitations.resend`` - Scope ``team:write``. + Scope ``team:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["team.invitations.resend"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -734,7 +734,7 @@ def revoke(self, id: str, *, approval: ApprovalMode = "wait", idempotency_key: s Requires scope `team:write` and the `manage-members` capability. The link stops working. An invitation that is not pending in this workspace is a `404`. ``DELETE /workspace/invitations/{id}`` · action ``team.invitations.revoke`` - Scope ``team:write``. + Scope ``team:write`` · danger: destructive. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["team.invitations.revoke"], (id,), None, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -781,7 +781,7 @@ def invite(self, body: TeamInviteBody, *, approval: ApprovalMode = "wait", idemp withdraw it under `/workspace/invitations`. ``POST /workspace/members`` · action ``team.invite`` - Scope ``team:write``. + Scope ``team:write`` · danger: write. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["team.invite"], (), body, approval=approval, idempotency_key=idempotency_key, approval_id=approval_id, headers=headers) @@ -798,7 +798,7 @@ def list(self, query: TeamListQuery | None = None, *, approval: ApprovalMode = " Requires scope `team:read` and the `read-members` capability (owner/admin/viewer). The roster is PII — a developer key is refused. ``GET /workspace/members`` · action ``team.list`` - Scope ``team:read``. + Scope ``team:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["team.list"], (), query, approval=approval, approval_id=approval_id, headers=headers) @@ -902,7 +902,7 @@ def get(self, *, approval: ApprovalMode = "wait", approval_id: str | None = None Requires scope `workspace:read` (any role). Returns the workspace's identity. The `projects` block (each project's plan/allowance) is included only for keys whose role can read billing (owner/admin/viewer — not developer). ``GET /workspace`` · action ``workspace.get`` - Scope ``workspace:read``. + Scope ``workspace:read`` · danger: read. May be held for approval (``202 approval_required``): waited on unless ``approval="return"``. """ return self._core.call(WORKSPACE_OPERATIONS["workspace.get"], (), None, approval=approval, approval_id=approval_id, headers=headers)