From 16bb288e9b09eb667f5d75d9090362480e0d86f3 Mon Sep 17 00:00:00 2001 From: dobrac <4323173+dobrac@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:31:17 +0000 Subject: [PATCH] chore: sync infra OpenAPI specs --- spec/openapi.dashboard-api.yaml | 463 +++++++++++--- spec/openapi.infra.yaml | 564 ++++++++++++++---- .../shared/contracts/dashboard-api.types.ts | 511 ++++++++++++---- src/core/shared/contracts/infra-api.types.ts | 458 ++++++++++---- 4 files changed, 1569 insertions(+), 427 deletions(-) diff --git a/spec/openapi.dashboard-api.yaml b/spec/openapi.dashboard-api.yaml index 09bbedd64..a0ed2fdd2 100644 --- a/spec/openapi.dashboard-api.yaml +++ b/spec/openapi.dashboard-api.yaml @@ -103,6 +103,30 @@ components: schema: type: string format: uuid + clusterID: + name: clusterID + in: path + required: true + description: Identifier of the cluster. + schema: + type: string + format: uuid + projectID: + name: projectID + in: path + required: true + description: Identifier of the project. + schema: + type: string + format: uuid + userID: + name: userID + in: path + required: true + description: Identifier of the user. + schema: + type: string + format: uuid userId: name: userId in: path @@ -400,6 +424,67 @@ components: format: email description: Billing/contact email for the team. + AdminClusterCreateRequest: + type: object + required: + - name + - endpoint + - endpoint_tls + - token + properties: + cluster_id: + type: string + format: uuid + description: Optional stable identifier for idempotent creation. Reuse succeeds only when the immutable configuration is identical. + name: + type: string + minLength: 1 + endpoint: + type: string + minLength: 1 + endpoint_tls: + type: boolean + token: + type: string + minLength: 1 + sandbox_proxy_domain: + type: string + nullable: true + auth_org_id: + type: string + nullable: true + + AdminClusterCreateResponse: + type: object + required: + - cluster_id + properties: + cluster_id: + type: string + format: uuid + + AdminTeamClusterAssignmentRequest: + type: object + required: + - cluster_id + properties: + cluster_id: + type: string + format: uuid + preserve_existing: + type: boolean + default: false + description: Assign only when the team is unassigned or already assigned to cluster_id. + + AdminTeamClusterAssignmentResponse: + type: object + required: + - cluster_id + properties: + cluster_id: + type: string + format: uuid + BuildStatus: type: string description: Build status mapped for dashboard clients. @@ -708,6 +793,21 @@ components: items: $ref: "#/components/schemas/UserTeam" + TeamStatusResponse: + type: object + required: + - isBlocked + - isBanned + - blockedReason + properties: + isBlocked: + type: boolean + isBanned: + type: boolean + blockedReason: + type: string + nullable: true + TeamMember: type: object required: @@ -1162,13 +1262,19 @@ components: slug: type: string - AdminControlPlaneProjectType: - type: string - enum: [development, staging, production] - - AdminControlPlaneProjectUpsertRequest: + ManagementProjectUpsertRequest: type: object - required: [name, slug, project_type] + description: >- + The properties of a project this side stores. Every one is synchronized + by the caller and sent on every push, so a reconcile is a complete + statement of the project rather than a patch. + + + A project's tier is not among them. It is assigned once, at creation, + from this side's own default, and no push moves it — limits arrive + separately and in full through upsertProjectLimits, which takes + precedence over the tier anyway. + required: [name, slug, email] properties: name: type: string @@ -1178,12 +1284,21 @@ components: type: string minLength: 1 maxLength: 63 - project_type: - $ref: "#/components/schemas/AdminControlPlaneProjectType" + description: >- + Changing it renames the project, and nothing follows it. Template + names embed the slug they were built under, so a renamed project + keeps its existing template names and only new ones carry the new + slug. A slug already held on this control plane is a 409, on a + rename as much as on a create. + email: + type: string + minLength: 1 + maxLength: 255 + description: Contact address recorded on the project. - AdminControlPlaneProject: + ManagementProject: allOf: - - $ref: "#/components/schemas/AdminControlPlaneProjectUpsertRequest" + - $ref: "#/components/schemas/ManagementProjectUpsertRequest" - type: object required: [id] properties: @@ -1191,16 +1306,50 @@ components: type: string format: uuid - AdminControlPlaneMemberUpsertRequest: + ManagementProjectMemberIdentity: type: object + required: [issuer, subject] properties: - added_by: + issuer: type: string - format: uuid + minLength: 1 + maxLength: 2048 + subject: + type: string + minLength: 1 + maxLength: 2048 - AdminControlPlaneProjectLimits: + ManagementProjectMemberApplyRequest: type: object + required: [revision, present] + properties: + revision: + type: integer + format: int64 + minimum: 1 + present: + type: boolean + identities: + type: array + maxItems: 16 + items: + $ref: "#/components/schemas/ManagementProjectMemberIdentity" + + ManagementProjectLimits: + type: object + description: >- + A project's effective limits, already resolved by the caller. Every + field is absolute: this side stores what it is given and performs no + arithmetic of its own. + + + The minimums below track the CHECK constraints on tiers, which is the + contract for what a limit may be. project_limits stores the same values + under looser constraints on purpose — it is a push target, and a floor + that only rejects the impossible keeps a future decision about what is + allowed a change to this schema rather than a migration. required: + - revision - concurrent_sandboxes - max_sandbox_length_hours - max_vcpu @@ -1208,7 +1357,23 @@ components: - disk_mb - concurrent_template_builds - events_ttl_days + - default_free_disk_size_mb + - max_disk_size_mb properties: + revision: + type: integer + format: int64 + minimum: 1 + description: >- + The caller's version of this answer, raised whenever the limits it + resolved for the project change. Delivery is over a network, so two + pushes can be in flight at once and arrive in either order: this + side stores the revision it accepted and drops a delivery at or + below it, which is what keeps a delayed retry from putting the + project back on limits it has already left. + + + Comparable only against earlier revisions for the same project. concurrent_sandboxes: type: integer format: int32 @@ -1237,14 +1402,25 @@ components: type: integer format: int32 minimum: 1 + default_free_disk_size_mb: + type: integer + format: int64 + minimum: 0 + description: >- + Free rootfs allowance. Must not exceed max_disk_size_mb; the two + are stored together and rejected as a pair if they disagree. + max_disk_size_mb: + type: integer + format: int64 + minimum: 1 tags: - name: builds + - name: control-plane-management + description: Workspace control-plane operations authenticated with service JWTs. - name: sandboxes - name: teams - name: templates - - name: workspace-admin - description: Workspace control-plane admin operations authenticated with service JWTs. paths: /health: @@ -1463,6 +1639,132 @@ paths: "500": $ref: "#/components/responses/500" + /admin/clusters: + post: + summary: Create a cluster + description: Creates a cluster whose configuration cannot be modified. + tags: [admin] + security: + - AdminApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AdminClusterCreateRequest" + responses: + "201": + description: Cluster created. + content: + application/json: + schema: + $ref: "#/components/schemas/AdminClusterCreateResponse" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "409": + $ref: "#/components/responses/409" + "500": + $ref: "#/components/responses/500" + + /admin/clusters/{clusterID}: + delete: + summary: Delete an unreferenced cluster + description: Deletes a cluster only after all team assignments are detached and no environment history references it. Repeating a completed deletion succeeds. + tags: [admin] + security: + - AdminApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/clusterID" + responses: + "204": + description: Cluster deleted or already absent. + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "409": + $ref: "#/components/responses/409" + "500": + $ref: "#/components/responses/500" + + /admin/teams/{teamID}/cluster: + get: + summary: Get a team's assigned cluster + description: Returns the current cluster assignment without exposing cluster credentials. + tags: [admin] + security: + - AdminApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/teamID" + responses: + "200": + description: Cluster assignment returned. + content: + application/json: + schema: + $ref: "#/components/schemas/AdminTeamClusterAssignmentResponse" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + put: + summary: Assign a cluster to a team + description: Updates the team's cluster reference to an existing cluster. + tags: [admin] + security: + - AdminApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/teamID" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AdminTeamClusterAssignmentRequest" + responses: + "204": + description: Cluster assigned. + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "500": + $ref: "#/components/responses/500" + + /admin/teams/{teamID}/cluster/{clusterID}: + delete: + summary: Detach a cluster from a team + description: Clears the assignment only when the team is unassigned or assigned to the specified cluster. + tags: [admin] + security: + - AdminApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/teamID" + - $ref: "#/components/parameters/clusterID" + responses: + "204": + description: Cluster detached or the team was already unassigned. + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "500": + $ref: "#/components/responses/500" + /admin/user-profiles/resolve: post: summary: Resolve user profiles @@ -1616,6 +1918,30 @@ paths: "500": $ref: "#/components/responses/500" + /teams/{teamID}/status: + get: + summary: Get team access status + description: Returns whether the team is blocked or banned and its recorded blocked reason. Bearer-authenticated users may read only teams they belong to. + tags: [teams] + security: + - AuthProviderBearerAuth: [] + - AdminApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/teamID" + responses: + "200": + description: Successfully returned team access status. + content: + application/json: + schema: + $ref: "#/components/schemas/TeamStatusResponse" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + /teams/{teamID}/members: get: summary: List team members @@ -1889,13 +2215,13 @@ paths: "500": $ref: "#/components/responses/500" - /admin/v1/projects/{teamID}: + /v1/management/projects/{projectID}: parameters: - - $ref: "#/components/parameters/teamID" + - $ref: "#/components/parameters/projectID" put: - operationId: upsertProject - summary: Create or reconcile a project. - tags: [workspace-admin] + operationId: managementUpsertProject + summary: Create or reconcile a project (v1). + tags: [control-plane-management] security: - AdminJWTAuth: [] requestBody: @@ -1903,20 +2229,20 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AdminControlPlaneProjectUpsertRequest" + $ref: "#/components/schemas/ManagementProjectUpsertRequest" responses: "200": description: Existing project reconciled. content: application/json: schema: - $ref: "#/components/schemas/AdminControlPlaneProject" + $ref: "#/components/schemas/ManagementProject" "201": description: Project created. content: application/json: schema: - $ref: "#/components/schemas/AdminControlPlaneProject" + $ref: "#/components/schemas/ManagementProject" "400": $ref: "#/components/responses/400" "401": @@ -1928,9 +2254,15 @@ paths: "501": $ref: "#/components/responses/501" delete: - operationId: deleteProject - summary: Delete a project and its control-plane state. - tags: [workspace-admin] + operationId: managementDeleteProject + summary: Delete a project and its control-plane state (v1). + description: >- + Declared, and answered with 501 by every control plane. Deleting a + project means reclaiming templates, snapshots, volumes, running + sandboxes and their stored artifacts, and no single service can reach + all of them today. Callers should not depend on this operation until + that changes. + tags: [control-plane-management] security: - AdminJWTAuth: [] responses: @@ -1945,62 +2277,46 @@ paths: "501": $ref: "#/components/responses/501" - /admin/v1/projects/{teamID}/members/{userId}: + /v1/management/projects/{projectID}/members/{userID}: parameters: - - $ref: "#/components/parameters/teamID" - - $ref: "#/components/parameters/userId" + - $ref: "#/components/parameters/projectID" + - $ref: "#/components/parameters/userID" put: - operationId: upsertProjectMember - summary: Reconcile an opaque user UUID as a project member. - tags: [workspace-admin] + operationId: managementApplyProjectMember + summary: Apply one versioned project member projection (v1). + description: >- + Applies the newest desired presence for one project member. An older + or duplicate revision is accepted without changing target state. + tags: [control-plane-management] security: - AdminJWTAuth: [] requestBody: - required: false + required: true content: application/json: schema: - $ref: "#/components/schemas/AdminControlPlaneMemberUpsertRequest" - responses: - "204": - description: Membership is present. - "400": - $ref: "#/components/responses/400" - "401": - $ref: "#/components/responses/401" - "404": - $ref: "#/components/responses/404" - "500": - $ref: "#/components/responses/500" - "501": - $ref: "#/components/responses/501" - delete: - operationId: deleteProjectMember - summary: Remove a project member. - tags: [workspace-admin] - security: - - AdminJWTAuth: [] + $ref: "#/components/schemas/ManagementProjectMemberApplyRequest" responses: "204": - description: Membership is absent. + description: Membership projection is applied or already superseded. "400": $ref: "#/components/responses/400" "401": $ref: "#/components/responses/401" "404": $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" "500": $ref: "#/components/responses/500" - "501": - $ref: "#/components/responses/501" - /admin/v1/projects/{teamID}/limits: + /v1/management/projects/{projectID}/limits: parameters: - - $ref: "#/components/parameters/teamID" + - $ref: "#/components/parameters/projectID" put: - operationId: upsertProjectLimits - summary: Reconcile effective limits for a project. - tags: [workspace-admin] + operationId: managementUpsertProjectLimits + summary: Reconcile effective limits for a project (v1). + tags: [control-plane-management] security: - AdminJWTAuth: [] requestBody: @@ -2008,7 +2324,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AdminControlPlaneProjectLimits" + $ref: "#/components/schemas/ManagementProjectLimits" responses: "204": description: Effective limits are synchronized. @@ -2022,24 +2338,3 @@ paths: $ref: "#/components/responses/500" "501": $ref: "#/components/responses/501" - - /admin/v1/users/{userId}: - parameters: - - $ref: "#/components/parameters/userId" - delete: - operationId: purgeUser - summary: Purge shard-local membership and access-token state for an opaque user UUID. - tags: [workspace-admin] - security: - - AdminJWTAuth: [] - responses: - "204": - description: User-owned shard state is absent. - "400": - $ref: "#/components/responses/400" - "401": - $ref: "#/components/responses/401" - "500": - $ref: "#/components/responses/500" - "501": - $ref: "#/components/responses/501" diff --git a/spec/openapi.infra.yaml b/spec/openapi.infra.yaml index 4a26b3a42..9c4f46a7b 100644 --- a/spec/openapi.infra.yaml +++ b/spec/openapi.infra.yaml @@ -12,14 +12,6 @@ components: type: apiKey in: header name: X-API-Key - AccessTokenAuth: - type: http - scheme: bearer - bearerFormat: access_token - description: | - **Deprecated.** Access token authentication is deprecated and will be - removed in a future release. Use API key authentication (`X-API-Key`) - instead. # AuthProviderBearerAuth / AuthProviderTeamAuth: B before T in the name # so Bearer is validated before Team. AuthProviderBearerAuth: @@ -78,12 +70,6 @@ components: required: true schema: type: string - accessTokenID: - name: accessTokenID - in: path - required: true - schema: - type: string snapshotID: name: snapshotID in: path @@ -122,6 +108,28 @@ components: required: true schema: type: string + secretID: + name: secretID + in: path + required: true + schema: + type: string + description: > + Identifier of the secret (sec_ prefixed), or its canonical + lower-case name + + headers: + XNextToken: + description: Cursor to fetch the next page of results, if more exist + schema: + type: string + XTotalRunning: + description: > + Number of running sandboxes matching the filters, before pagination is applied. + Only present when running sandboxes were requested. + schema: + type: integer + format: int32 responses: "400": @@ -154,8 +162,8 @@ components: application/json: schema: $ref: "#/components/schemas/Error" - "410": - description: Gone + "429": + description: Too many requests content: application/json: schema: @@ -166,6 +174,24 @@ components: application/json: schema: $ref: "#/components/schemas/Error" + "502": + description: Backend error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "503": + description: Service unavailable + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "504": + description: Backend timeout + content: + application/json: + schema: + $ref: "#/components/schemas/Error" schemas: Team: @@ -254,6 +280,14 @@ components: - running - paused + OrderDirection: + type: string + description: Sort direction + default: desc + enum: + - asc + - desc + SnapshotInfo: type: object required: @@ -800,6 +834,16 @@ components: type: boolean deprecated: true description: Automatically pauses the sandbox after the timeout + memory: + type: boolean + x-not-implemented: true + description: >- + Defaults to true. When false, resume from disk state only: the sandbox cold-boots fresh and + any memory in the snapshot is ignored, never modified or deleted. Disk + state has crash-recovery semantics — writes not flushed before the pause + may be lost. A no-op for snapshots that contain no memory. Rejected with + an error in environments where this capability is not enabled, never + silently downgraded to a memory restore. ConnectSandbox: type: object @@ -811,6 +855,16 @@ components: type: integer format: int32 minimum: 0 + memory: + type: boolean + x-not-implemented: true + description: >- + Defaults to true. When false and the sandbox is paused, resume from disk state only: the + sandbox cold-boots fresh and any memory in the snapshot is ignored, never + modified or deleted. Disk state has crash-recovery semantics — writes not + flushed before the pause may be lost. A no-op for snapshots that contain + no memory. Rejected with an error in environments where this capability + is not enabled, never silently downgraded to a memory restore. SandboxTimeoutRequest: type: object @@ -944,6 +998,17 @@ components: type: integer description: Number of sandboxes that failed to kill + AdminTeamRunningSandboxCounts: + type: object + description: | + Cached live sandbox index count keyed by team ID. Counts may briefly + include sandboxes transitioning out of running; teams without indexed + sandboxes are omitted. + additionalProperties: + type: integer + format: int64 + minimum: 1 + AdminBuildCancelResult: required: - cancelledCount @@ -1749,7 +1814,6 @@ components: - status - statusChangedAt - sandboxCount - - cachedBuilds - createSuccesses - createFails - version @@ -1786,11 +1850,6 @@ components: description: Number of sandboxes running on the node metrics: $ref: "#/components/schemas/NodeMetrics" - cachedBuilds: - type: array - description: List of cached builds id on the node - items: - type: string createSuccesses: type: integer format: uint64 @@ -1800,39 +1859,6 @@ components: format: uint64 description: Number of sandbox create fails - CreatedAccessToken: - required: - - id - - name - - token - - mask - - createdAt - properties: - id: - type: string - format: uuid - description: Identifier of the access token - name: - type: string - description: Name of the access token - token: - type: string - description: The fully created access token - mask: - $ref: "#/components/schemas/IdentifierMaskingDetails" - createdAt: - type: string - format: date-time - description: Timestamp of access token creation - - NewAccessToken: - required: - - name - properties: - name: - type: string - description: Name of the access token - TeamAPIKey: required: - id @@ -1983,6 +2009,12 @@ components: type: integer format: int32 description: Error code + error_code: + type: string + description: >- + Machine-readable semantic error code. Not a closed set; initial values: + sandbox_capacity_unavailable, sandbox_placement_timeout, + sandbox_no_compatible_node, sandbox_create_failed, internal_server_error. message: type: string description: Error @@ -2032,6 +2064,13 @@ components: token: type: string description: Auth token to use for interacting with volume content + domain: + type: string + description: | + Domain to use as the destination for volume content requests, + replacing the default `api.`. Only returned when the + team is connected to a custom (BYOC) cluster; absent otherwise, in + which case the default domain is used. required: - volumeID - name @@ -2047,14 +2086,89 @@ components: required: - name + SecretMetadata: + type: object + description: > + Customer metadata of the secret. Always present, empty when unset. + At most 32 entries; keys are limited to 128 bytes, values to 1024 + bytes, and a secret's metadata to 8192 bytes in total. + maxProperties: 32 + additionalProperties: + type: string + maxLength: 1024 + + Secret: + type: object + description: Metadata of a secret. It never carries the secret value. + required: + - secretID + - name + - currentVersion + - metadata + - createdAt + - updatedAt + properties: + secretID: + type: string + description: Identifier of the secret + name: + type: string + description: Name of the secret, unique within the project + currentVersion: + type: integer + format: int64 + description: Version served to readers that do not name one + metadata: + $ref: "#/components/schemas/SecretMetadata" + createdAt: + type: string + format: date-time + description: Time when the secret was created + updatedAt: + type: string + format: date-time + description: Time when the secret was last updated + + NewSecret: + type: object + required: + - name + - value + properties: + name: + type: string + minLength: 1 + maxLength: 128 + pattern: '^[a-zA-Z0-9_-]+$' + description: > + Name of the secret, unique within the project. Names are + lower-cased before storage and returned in that canonical form; + the sec_ prefix is reserved for secret identifiers. + value: + type: string + description: Runtime marker stored as the secret's first version. The runtime resolves it to a value at sandbox egress. + metadata: + $ref: "#/components/schemas/SecretMetadata" + + SecretUpdate: + type: object + required: + - value + properties: + value: + type: string + description: Runtime marker stored as the secret's new version. The runtime resolves it to a value at sandbox egress. + metadata: + $ref: "#/components/schemas/SecretMetadata" + tags: - name: templates - name: sandboxes - name: auth - - name: access-tokens - name: api-keys - name: tags - name: volumes + - name: secrets paths: /health: @@ -2073,7 +2187,6 @@ paths: description: List all teams tags: [auth] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] responses: "200": @@ -2247,6 +2360,10 @@ paths: $ref: "#/components/responses/400" "500": $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + "504": + $ref: "#/components/responses/504" /v2/sandboxes: get: @@ -2276,11 +2393,35 @@ paths: $ref: "#/components/schemas/SandboxState" style: form explode: false + - name: order + in: query + description: Sort direction by sandbox start time. Defaults to desc (newest first). + required: false + schema: + $ref: "#/components/schemas/OrderDirection" + - name: startedAfter + in: query + description: Return sandboxes started at or after this timestamp. + required: false + schema: + type: string + format: date-time + - name: template + in: query + description: Filter sandboxes by a template ID or alias. + required: false + schema: + type: string - $ref: "#/components/parameters/paginationNextToken" - $ref: "#/components/parameters/paginationLimit" responses: "200": description: Successfully returned all running sandboxes + headers: + X-Next-Token: + $ref: "#/components/headers/XNextToken" + X-Total-Running: + $ref: "#/components/headers/XTotalRunning" content: application/json: schema: @@ -2575,7 +2716,7 @@ paths: parameters: - $ref: "#/components/parameters/sandboxID" requestBody: - required: true + required: false content: application/json: schema: @@ -2591,10 +2732,16 @@ paths: $ref: "#/components/responses/409" "404": $ref: "#/components/responses/404" + "400": + $ref: "#/components/responses/400" "401": $ref: "#/components/responses/401" "500": $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + "504": + $ref: "#/components/responses/504" /sandboxes/{sandboxID}/fork: post: @@ -2641,6 +2788,8 @@ paths: $ref: "#/components/responses/401" "500": $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" /sandboxes/{sandboxID}/connect: post: @@ -2680,8 +2829,14 @@ paths: $ref: "#/components/responses/401" "404": $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" "500": $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + "504": + $ref: "#/components/responses/504" /sandboxes/{sandboxID}/timeout: post: @@ -2832,6 +2987,9 @@ paths: responses: "200": description: Successfully returned snapshots + headers: + X-Next-Token: + $ref: "#/components/headers/XNextToken" content: application/json: schema: @@ -2884,7 +3042,6 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] @@ -2903,9 +3060,7 @@ paths: description: Successfully returned all templates headers: X-Next-Token: - description: Cursor to fetch the next page of results, if more exist - schema: - type: string + $ref: "#/components/headers/XNextToken" content: application/json: schema: @@ -2958,7 +3113,6 @@ paths: description: Get an upload link for a tar file containing build layer files tags: [templates] security: - - AccessTokenAuth: [] - ApiKeyAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] @@ -2997,7 +3151,6 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] @@ -3028,7 +3181,6 @@ paths: deprecated: true tags: [templates] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] requestBody: @@ -3070,6 +3222,9 @@ paths: responses: "200": description: Successfully returned the template with its builds + headers: + X-Next-Token: + $ref: "#/components/headers/XNextToken" content: application/json: schema: @@ -3084,7 +3239,6 @@ paths: deprecated: true tags: [templates] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] parameters: @@ -3113,7 +3267,6 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] @@ -3134,7 +3287,6 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] @@ -3164,7 +3316,6 @@ paths: deprecated: true tags: [templates] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] parameters: @@ -3213,7 +3364,6 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] @@ -3246,7 +3396,6 @@ paths: description: Get template build info tags: [templates] security: - - AccessTokenAuth: [] - ApiKeyAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] @@ -3296,7 +3445,6 @@ paths: description: Get template build logs tags: [templates] security: - - AccessTokenAuth: [] - ApiKeyAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] @@ -3585,6 +3733,27 @@ paths: "500": $ref: "#/components/responses/500" + /admin/sandboxes/running-counts: + get: + summary: Count running sandboxes by team + description: | + Returns a shared snapshot normally refreshed after five seconds. A + sandbox transitioning out of running can remain counted until removal. + tags: [admin] + security: + - AdminApiKeyAuth: [] + responses: + "200": + description: Running sandbox counts keyed by team ID + content: + application/json: + schema: + $ref: "#/components/schemas/AdminTeamRunningSandboxCounts" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + /admin/teams/{teamID}/builds/cancel: post: summary: Cancel all builds for a team @@ -3681,53 +3850,6 @@ paths: "500": $ref: "#/components/responses/500" - /access-tokens: - post: - summary: Create access token - description: Create a new access token. Deprecated; use an API key (E2B_API_KEY) instead. - deprecated: true - tags: [access-tokens] - security: - - AuthProviderBearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/NewAccessToken" - responses: - "201": - description: Access token created successfully - content: - application/json: - schema: - $ref: "#/components/schemas/CreatedAccessToken" - "401": - $ref: "#/components/responses/401" - "410": - $ref: "#/components/responses/410" - "500": - $ref: "#/components/responses/500" - - /access-tokens/{accessTokenID}: - delete: - summary: Delete access token - description: Delete an access token - tags: [access-tokens] - security: - - AuthProviderBearerAuth: [] - parameters: - - $ref: "#/components/parameters/accessTokenID" - responses: - "204": - description: Access token deleted successfully - "401": - $ref: "#/components/responses/401" - "404": - $ref: "#/components/responses/404" - "500": - $ref: "#/components/responses/500" - /api-keys: get: summary: List team API keys @@ -3927,3 +4049,207 @@ paths: $ref: "#/components/responses/404" "500": $ref: "#/components/responses/500" + + /secrets: + get: + summary: List project secrets + description: List the project's secrets. No response carries a secret value. + tags: [secrets] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/paginationNextToken" + - $ref: "#/components/parameters/paginationLimit" + responses: + "200": + description: Successfully listed the project's secrets + headers: + X-Next-Token: + $ref: "#/components/headers/XNextToken" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Secret" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "502": + $ref: "#/components/responses/502" + "504": + $ref: "#/components/responses/504" + + post: + summary: Create a secret + description: Create a secret by storing a runtime marker as its first version. The response carries metadata only. + tags: [secrets] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewSecret" + responses: + "201": + description: Successfully created the secret + content: + application/json: + schema: + $ref: "#/components/schemas/Secret" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "502": + $ref: "#/components/responses/502" + "504": + $ref: "#/components/responses/504" + + /secrets/{secretID}: + get: + summary: Get a secret + description: Get one secret's metadata, selected by identifier or name. + tags: [secrets] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/secretID" + responses: + "200": + description: Successfully retrieved the secret + content: + application/json: + schema: + $ref: "#/components/schemas/Secret" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "502": + $ref: "#/components/responses/502" + "504": + $ref: "#/components/responses/504" + + post: + summary: Update a secret + description: Replace the secret's stored marker by appending a new version. The response carries metadata only. + tags: [secrets] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/secretID" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SecretUpdate" + responses: + "200": + description: Successfully updated the secret + content: + application/json: + schema: + $ref: "#/components/schemas/Secret" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "502": + $ref: "#/components/responses/502" + "504": + $ref: "#/components/responses/504" + + delete: + summary: Delete a secret + description: Revoke the secret and schedule its versions for cleanup. + tags: [secrets] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/secretID" + responses: + "204": + description: Successfully deleted the secret + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "403": + $ref: "#/components/responses/403" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "502": + $ref: "#/components/responses/502" + "504": + $ref: "#/components/responses/504" diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts index abe271801..097a82266 100644 --- a/src/core/shared/contracts/dashboard-api.types.ts +++ b/src/core/shared/contracts/dashboard-api.types.ts @@ -376,6 +376,223 @@ export interface paths { patch?: never trace?: never } + '/admin/clusters': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Create a cluster + * @description Creates a cluster whose configuration cannot be modified. + */ + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AdminClusterCreateRequest'] + } + } + responses: { + /** @description Cluster created. */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AdminClusterCreateResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 409: components['responses']['409'] + 500: components['responses']['500'] + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/admin/clusters/{clusterID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** + * Delete an unreferenced cluster + * @description Deletes a cluster only after all team assignments are detached and no environment history references it. Repeating a completed deletion succeeds. + */ + delete: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the cluster. */ + clusterID: components['parameters']['clusterID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Cluster deleted or already absent. */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 409: components['responses']['409'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } + '/admin/teams/{teamID}/cluster': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get a team's assigned cluster + * @description Returns the current cluster assignment without exposing cluster credentials. + */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamID: components['parameters']['teamID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Cluster assignment returned. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AdminTeamClusterAssignmentResponse'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + /** + * Assign a cluster to a team + * @description Updates the team's cluster reference to an existing cluster. + */ + put: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamID: components['parameters']['teamID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AdminTeamClusterAssignmentRequest'] + } + } + responses: { + /** @description Cluster assigned. */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 500: components['responses']['500'] + } + } + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/admin/teams/{teamID}/cluster/{clusterID}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** + * Detach a cluster from a team + * @description Clears the assignment only when the team is unassigned or assigned to the specified cluster. + */ + delete: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamID: components['parameters']['teamID'] + /** @description Identifier of the cluster. */ + clusterID: components['parameters']['clusterID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Cluster detached or the team was already unassigned. */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 500: components['responses']['500'] + } + } + options?: never + head?: never + patch?: never + trace?: never + } '/admin/user-profiles/resolve': { parameters: { query?: never @@ -642,6 +859,51 @@ export interface paths { } trace?: never } + '/teams/{teamID}/status': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Get team access status + * @description Returns whether the team is blocked or banned and its recorded blocked reason. Bearer-authenticated users may read only teams they belong to. + */ + get: { + parameters: { + query?: never + header?: never + path: { + /** @description Identifier of the team. */ + teamID: components['parameters']['teamID'] + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned team access status. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamStatusResponse'] + } + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/teams/{teamID}/members': { parameters: { query?: never @@ -1106,63 +1368,48 @@ export interface paths { patch?: never trace?: never } - '/admin/v1/projects/{teamID}': { + '/v1/management/projects/{projectID}': { parameters: { query?: never header?: never path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] } cookie?: never } get?: never - /** Create or reconcile a project. */ - put: operations['upsertProject'] + /** Create or reconcile a project (v1). */ + put: operations['managementUpsertProject'] post?: never - /** Delete a project and its control-plane state. */ - delete: operations['deleteProject'] + /** + * Delete a project and its control-plane state (v1). + * @description Declared, and answered with 501 by every control plane. Deleting a project means reclaiming templates, snapshots, volumes, running sandboxes and their stored artifacts, and no single service can reach all of them today. Callers should not depend on this operation until that changes. + */ + delete: operations['managementDeleteProject'] options?: never head?: never patch?: never trace?: never } - '/admin/v1/projects/{teamID}/members/{userId}': { + '/v1/management/projects/{projectID}/members/{userID}': { parameters: { query?: never header?: never path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] /** @description Identifier of the user. */ - userId: components['parameters']['userId'] - } - cookie?: never - } - get?: never - /** Reconcile an opaque user UUID as a project member. */ - put: operations['upsertProjectMember'] - post?: never - /** Remove a project member. */ - delete: operations['deleteProjectMember'] - options?: never - head?: never - patch?: never - trace?: never - } - '/admin/v1/projects/{teamID}/limits': { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + userID: components['parameters']['userID'] } cookie?: never } get?: never - /** Reconcile effective limits for a project. */ - put: operations['upsertProjectLimits'] + /** + * Apply one versioned project member projection (v1). + * @description Applies the newest desired presence for one project member. An older or duplicate revision is accepted without changing target state. + */ + put: operations['managementApplyProjectMember'] post?: never delete?: never options?: never @@ -1170,21 +1417,21 @@ export interface paths { patch?: never trace?: never } - '/admin/v1/users/{userId}': { + '/v1/management/projects/{projectID}/limits': { parameters: { query?: never header?: never path: { - /** @description Identifier of the user. */ - userId: components['parameters']['userId'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] } cookie?: never } get?: never - put?: never + /** Reconcile effective limits for a project (v1). */ + put: operations['managementUpsertProjectLimits'] post?: never - /** Purge shard-local membership and access-token state for an opaque user UUID. */ - delete: operations['purgeUser'] + delete?: never options?: never head?: never patch?: never @@ -1240,6 +1487,36 @@ export interface components { */ email: string } + AdminClusterCreateRequest: { + /** + * Format: uuid + * @description Optional stable identifier for idempotent creation. Reuse succeeds only when the immutable configuration is identical. + */ + cluster_id?: string + name: string + endpoint: string + endpoint_tls: boolean + token: string + sandbox_proxy_domain?: string | null + auth_org_id?: string | null + } + AdminClusterCreateResponse: { + /** Format: uuid */ + cluster_id: string + } + AdminTeamClusterAssignmentRequest: { + /** Format: uuid */ + cluster_id: string + /** + * @description Assign only when the team is unassigned or already assigned to cluster_id. + * @default false + */ + preserve_existing: boolean + } + AdminTeamClusterAssignmentResponse: { + /** Format: uuid */ + cluster_id: string + } /** * @description Build status mapped for dashboard clients. * @enum {string} @@ -1408,6 +1685,11 @@ export interface components { UserTeamsResponse: { teams: components['schemas']['UserTeam'][] } + TeamStatusResponse: { + isBlocked: boolean + isBanned: boolean + blockedReason: string | null + } TeamMember: { /** Format: uuid */ id: string @@ -1619,22 +1901,45 @@ export interface components { id: string slug: string } - /** @enum {string} */ - AdminControlPlaneProjectType: 'development' | 'staging' | 'production' - AdminControlPlaneProjectUpsertRequest: { + /** + * @description The properties of a project this side stores. Every one is synchronized by the caller and sent on every push, so a reconcile is a complete statement of the project rather than a patch. + * + * A project's tier is not among them. It is assigned once, at creation, from this side's own default, and no push moves it — limits arrive separately and in full through upsertProjectLimits, which takes precedence over the tier anyway. + */ + ManagementProjectUpsertRequest: { name: string + /** @description Changing it renames the project, and nothing follows it. Template names embed the slug they were built under, so a renamed project keeps its existing template names and only new ones carry the new slug. A slug already held on this control plane is a 409, on a rename as much as on a create. */ slug: string - project_type: components['schemas']['AdminControlPlaneProjectType'] + /** @description Contact address recorded on the project. */ + email: string } - AdminControlPlaneProject: components['schemas']['AdminControlPlaneProjectUpsertRequest'] & { + ManagementProject: components['schemas']['ManagementProjectUpsertRequest'] & { /** Format: uuid */ id: string } - AdminControlPlaneMemberUpsertRequest: { - /** Format: uuid */ - added_by?: string + ManagementProjectMemberIdentity: { + issuer: string + subject: string + } + ManagementProjectMemberApplyRequest: { + /** Format: int64 */ + revision: number + present: boolean + identities?: components['schemas']['ManagementProjectMemberIdentity'][] } - AdminControlPlaneProjectLimits: { + /** + * @description A project's effective limits, already resolved by the caller. Every field is absolute: this side stores what it is given and performs no arithmetic of its own. + * + * The minimums below track the CHECK constraints on tiers, which is the contract for what a limit may be. project_limits stores the same values under looser constraints on purpose — it is a push target, and a floor that only rejects the impossible keeps a future decision about what is allowed a change to this schema rather than a migration. + */ + ManagementProjectLimits: { + /** + * Format: int64 + * @description The caller's version of this answer, raised whenever the limits it resolved for the project change. Delivery is over a network, so two pushes can be in flight at once and arrive in either order: this side stores the revision it accepted and drops a delivery at or below it, which is what keeps a delayed retry from putting the project back on limits it has already left. + * + * Comparable only against earlier revisions for the same project. + */ + revision: number /** Format: int32 */ concurrent_sandboxes: number /** Format: int32 */ @@ -1649,6 +1954,13 @@ export interface components { concurrent_template_builds: number /** Format: int32 */ events_ttl_days: number + /** + * Format: int64 + * @description Free rootfs allowance. Must not exceed max_disk_size_mb; the two are stored together and rejected as a pair if they disagree. + */ + default_free_disk_size_mb: number + /** Format: int64 */ + max_disk_size_mb: number } } responses: { @@ -1742,6 +2054,12 @@ export interface components { build_ids: string[] /** @description Identifier of the team. */ teamID: string + /** @description Identifier of the cluster. */ + clusterID: string + /** @description Identifier of the project. */ + projectID: string + /** @description Identifier of the user. */ + userID: string /** @description Identifier of the user. */ userId: string /** @description Team slug to resolve. */ @@ -1787,19 +2105,19 @@ export interface components { } export type $defs = Record export interface operations { - upsertProject: { + managementUpsertProject: { parameters: { query?: never header?: never path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['AdminControlPlaneProjectUpsertRequest'] + 'application/json': components['schemas']['ManagementProjectUpsertRequest'] } } responses: { @@ -1809,7 +2127,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['AdminControlPlaneProject'] + 'application/json': components['schemas']['ManagementProject'] } } /** @description Project created. */ @@ -1818,7 +2136,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['AdminControlPlaneProject'] + 'application/json': components['schemas']['ManagementProject'] } } 400: components['responses']['400'] @@ -1828,13 +2146,13 @@ export interface operations { 501: components['responses']['501'] } } - deleteProject: { + managementDeleteProject: { parameters: { query?: never header?: never path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] } cookie?: never } @@ -1853,53 +2171,25 @@ export interface operations { 501: components['responses']['501'] } } - upsertProjectMember: { + managementApplyProjectMember: { parameters: { query?: never header?: never path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] /** @description Identifier of the user. */ - userId: components['parameters']['userId'] + userID: components['parameters']['userID'] } cookie?: never } - requestBody?: { + requestBody: { content: { - 'application/json': components['schemas']['AdminControlPlaneMemberUpsertRequest'] - } - } - responses: { - /** @description Membership is present. */ - 204: { - headers: { - [name: string]: unknown - } - content?: never + 'application/json': components['schemas']['ManagementProjectMemberApplyRequest'] } - 400: components['responses']['400'] - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - 501: components['responses']['501'] } - } - deleteProjectMember: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] - /** @description Identifier of the user. */ - userId: components['parameters']['userId'] - } - cookie?: never - } - requestBody?: never responses: { - /** @description Membership is absent. */ + /** @description Membership projection is applied or already superseded. */ 204: { headers: { [name: string]: unknown @@ -1909,23 +2199,23 @@ export interface operations { 400: components['responses']['400'] 401: components['responses']['401'] 404: components['responses']['404'] + 409: components['responses']['409'] 500: components['responses']['500'] - 501: components['responses']['501'] } } - upsertProjectLimits: { + managementUpsertProjectLimits: { parameters: { query?: never header?: never path: { - /** @description Identifier of the team. */ - teamID: components['parameters']['teamID'] + /** @description Identifier of the project. */ + projectID: components['parameters']['projectID'] } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['AdminControlPlaneProjectLimits'] + 'application/json': components['schemas']['ManagementProjectLimits'] } } responses: { @@ -1943,29 +2233,4 @@ export interface operations { 501: components['responses']['501'] } } - purgeUser: { - parameters: { - query?: never - header?: never - path: { - /** @description Identifier of the user. */ - userId: components['parameters']['userId'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description User-owned shard state is absent. */ - 204: { - headers: { - [name: string]: unknown - } - content?: never - } - 400: components['responses']['400'] - 401: components['responses']['401'] - 500: components['responses']['500'] - 501: components['responses']['501'] - } - } } diff --git a/src/core/shared/contracts/infra-api.types.ts b/src/core/shared/contracts/infra-api.types.ts index 4e306d805..43a72e38b 100644 --- a/src/core/shared/contracts/infra-api.types.ts +++ b/src/core/shared/contracts/infra-api.types.ts @@ -251,6 +251,8 @@ export interface paths { 400: components['responses']['400'] 401: components['responses']['401'] 500: components['responses']['500'] + 503: components['responses']['503'] + 504: components['responses']['504'] } } delete?: never @@ -277,6 +279,12 @@ export interface paths { metadata?: string /** @description Filter sandboxes by one or more states */ state?: components['schemas']['SandboxState'][] + /** @description Sort direction by sandbox start time. Defaults to desc (newest first). */ + order?: components['schemas']['OrderDirection'] + /** @description Return sandboxes started at or after this timestamp. */ + startedAfter?: string + /** @description Filter sandboxes by a template ID or alias. */ + template?: string /** @description Cursor to start the list from */ nextToken?: components['parameters']['paginationNextToken'] /** @description Maximum number of items to return per page */ @@ -291,6 +299,8 @@ export interface paths { /** @description Successfully returned all running sandboxes */ 200: { headers: { + 'X-Next-Token': components['headers']['XNextToken'] + 'X-Total-Running': components['headers']['XTotalRunning'] [name: string]: unknown } content: { @@ -649,7 +659,7 @@ export interface paths { } cookie?: never } - requestBody: { + requestBody?: { content: { 'application/json': components['schemas']['ResumedSandbox'] } @@ -664,10 +674,13 @@ export interface paths { 'application/json': components['schemas']['Sandbox'] } } + 400: components['responses']['400'] 401: components['responses']['401'] 404: components['responses']['404'] 409: components['responses']['409'] 500: components['responses']['500'] + 503: components['responses']['503'] + 504: components['responses']['504'] } } delete?: never @@ -717,6 +730,7 @@ export interface paths { 404: components['responses']['404'] 409: components['responses']['409'] 500: components['responses']['500'] + 503: components['responses']['503'] } } delete?: never @@ -774,7 +788,10 @@ export interface paths { 400: components['responses']['400'] 401: components['responses']['401'] 404: components['responses']['404'] + 409: components['responses']['409'] 500: components['responses']['500'] + 503: components['responses']['503'] + 504: components['responses']['504'] } } delete?: never @@ -1001,6 +1018,7 @@ export interface paths { /** @description Successfully returned snapshots */ 200: { headers: { + 'X-Next-Token': components['headers']['XNextToken'] [name: string]: unknown } content: { @@ -1095,8 +1113,7 @@ export interface paths { /** @description Successfully returned all templates */ 200: { headers: { - /** @description Cursor to fetch the next page of results, if more exist */ - 'X-Next-Token'?: string + 'X-Next-Token': components['headers']['XNextToken'] [name: string]: unknown } content: { @@ -1299,6 +1316,7 @@ export interface paths { /** @description Successfully returned the template with its builds */ 200: { headers: { + 'X-Next-Token': components['headers']['XNextToken'] [name: string]: unknown } content: { @@ -1980,6 +1998,48 @@ export interface paths { patch?: never trace?: never } + '/admin/sandboxes/running-counts': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Count running sandboxes by team + * @description Returns a shared snapshot normally refreshed after five seconds. A + * sandbox transitioning out of running can remain counted until removal. + */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Running sandbox counts keyed by team ID */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AdminTeamRunningSandboxCounts'] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/admin/teams/{teamID}/builds/cancel': { parameters: { query?: never @@ -2121,19 +2181,43 @@ export interface paths { patch?: never trace?: never } - '/access-tokens': { + '/api-keys': { parameters: { query?: never header?: never path?: never cookie?: never } - get?: never + /** + * List team API keys + * @description List all team API keys + */ + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description Successfully returned all team API keys */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['TeamAPIKey'][] + } + } + 401: components['responses']['401'] + 500: components['responses']['500'] + } + } put?: never /** - * Create access token - * @deprecated - * @description Create a new access token. Deprecated; use an API key (E2B_API_KEY) instead. + * Create team API key + * @description Create a new team API key */ post: { parameters: { @@ -2144,21 +2228,20 @@ export interface paths { } requestBody: { content: { - 'application/json': components['schemas']['NewAccessToken'] + 'application/json': components['schemas']['NewTeamAPIKey'] } } responses: { - /** @description Access token created successfully */ + /** @description Team API key created successfully */ 201: { headers: { [name: string]: unknown } content: { - 'application/json': components['schemas']['CreatedAccessToken'] + 'application/json': components['schemas']['CreatedTeamAPIKey'] } } 401: components['responses']['401'] - 410: components['responses']['410'] 500: components['responses']['500'] } } @@ -2168,7 +2251,7 @@ export interface paths { patch?: never trace?: never } - '/access-tokens/{accessTokenID}': { + '/api-keys/{apiKeyID}': { parameters: { query?: never header?: never @@ -2179,21 +2262,21 @@ export interface paths { put?: never post?: never /** - * Delete access token - * @description Delete an access token + * Delete team API key + * @description Delete a team API key */ delete: { parameters: { query?: never header?: never path: { - accessTokenID: components['parameters']['accessTokenID'] + apiKeyID: components['parameters']['apiKeyID'] } cookie?: never } requestBody?: never responses: { - /** @description Access token deleted successfully */ + /** @description Team API key deleted successfully */ 204: { headers: { [name: string]: unknown @@ -2207,10 +2290,40 @@ export interface paths { } options?: never head?: never - patch?: never + /** + * Update team API key + * @description Update a team API key + */ + patch: { + parameters: { + query?: never + header?: never + path: { + apiKeyID: components['parameters']['apiKeyID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateTeamAPIKey'] + } + } + responses: { + /** @description Team API key updated successfully */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + 401: components['responses']['401'] + 404: components['responses']['404'] + 500: components['responses']['500'] + } + } trace?: never } - '/api-keys': { + '/volumes': { parameters: { query?: never header?: never @@ -2218,8 +2331,8 @@ export interface paths { cookie?: never } /** - * List team API keys - * @description List all team API keys + * List team volumes + * @description List all team volumes */ get: { parameters: { @@ -2230,13 +2343,13 @@ export interface paths { } requestBody?: never responses: { - /** @description Successfully returned all team API keys */ + /** @description Successfully listed all team volumes */ 200: { headers: { [name: string]: unknown } content: { - 'application/json': components['schemas']['TeamAPIKey'][] + 'application/json': components['schemas']['Volume'][] } } 401: components['responses']['401'] @@ -2245,8 +2358,8 @@ export interface paths { } put?: never /** - * Create team API key - * @description Create a new team API key + * Create team volume + * @description Create a new team volume */ post: { parameters: { @@ -2257,19 +2370,20 @@ export interface paths { } requestBody: { content: { - 'application/json': components['schemas']['NewTeamAPIKey'] + 'application/json': components['schemas']['NewVolume'] } } responses: { - /** @description Team API key created successfully */ + /** @description Successfully created a new team volume */ 201: { headers: { [name: string]: unknown } content: { - 'application/json': components['schemas']['CreatedTeamAPIKey'] + 'application/json': components['schemas']['VolumeAndToken'] } } + 400: components['responses']['400'] 401: components['responses']['401'] 500: components['responses']['500'] } @@ -2280,66 +2394,61 @@ export interface paths { patch?: never trace?: never } - '/api-keys/{apiKeyID}': { + '/volumes/{volumeID}': { parameters: { query?: never header?: never path?: never cookie?: never } - get?: never - put?: never - post?: never /** - * Delete team API key - * @description Delete a team API key + * Team volume + * @description Get team volume info */ - delete: { + get: { parameters: { query?: never header?: never path: { - apiKeyID: components['parameters']['apiKeyID'] + volumeID: components['parameters']['volumeID'] } cookie?: never } requestBody?: never responses: { - /** @description Team API key deleted successfully */ - 204: { + /** @description Successfully retrieved a team volume */ + 200: { headers: { [name: string]: unknown } - content?: never + content: { + 'application/json': components['schemas']['VolumeAndToken'] + } } 401: components['responses']['401'] 404: components['responses']['404'] 500: components['responses']['500'] } } - options?: never - head?: never + put?: never + post?: never /** - * Update team API key - * @description Update a team API key + * Delete team volume + * @description Delete a team volume */ - patch: { + delete: { parameters: { query?: never header?: never path: { - apiKeyID: components['parameters']['apiKeyID'] + volumeID: components['parameters']['volumeID'] } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['UpdateTeamAPIKey'] - } - } + requestBody?: never responses: { - /** @description Team API key updated successfully */ - 200: { + /** @description Successfully deleted a team volume */ + 204: { headers: { [name: string]: unknown } @@ -2350,9 +2459,12 @@ export interface paths { 500: components['responses']['500'] } } + options?: never + head?: never + patch?: never trace?: never } - '/volumes': { + '/secrets': { parameters: { query?: never header?: never @@ -2360,35 +2472,48 @@ export interface paths { cookie?: never } /** - * List team volumes - * @description List all team volumes + * List project secrets + * @description List the project's secrets. No response carries a secret value. */ get: { parameters: { - query?: never + query?: { + /** @description Cursor to start the list from */ + nextToken?: components['parameters']['paginationNextToken'] + /** @description Maximum number of items to return per page */ + limit?: components['parameters']['paginationLimit'] + } header?: never path?: never cookie?: never } requestBody?: never responses: { - /** @description Successfully listed all team volumes */ + /** @description Successfully listed the project's secrets */ 200: { headers: { + 'X-Next-Token': components['headers']['XNextToken'] [name: string]: unknown } content: { - 'application/json': components['schemas']['Volume'][] + 'application/json': components['schemas']['Secret'][] } } + 400: components['responses']['400'] 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 429: components['responses']['429'] 500: components['responses']['500'] + 502: components['responses']['502'] + 504: components['responses']['504'] } } put?: never /** - * Create team volume - * @description Create a new team volume + * Create a secret + * @description Create a secret by storing a runtime marker as its first version. The response carries metadata only. */ post: { parameters: { @@ -2399,22 +2524,28 @@ export interface paths { } requestBody: { content: { - 'application/json': components['schemas']['NewVolume'] + 'application/json': components['schemas']['NewSecret'] } } responses: { - /** @description Successfully created a new team volume */ + /** @description Successfully created the secret */ 201: { headers: { [name: string]: unknown } content: { - 'application/json': components['schemas']['VolumeAndToken'] + 'application/json': components['schemas']['Secret'] } } 400: components['responses']['400'] 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 429: components['responses']['429'] 500: components['responses']['500'] + 502: components['responses']['502'] + 504: components['responses']['504'] } } delete?: never @@ -2423,7 +2554,7 @@ export interface paths { patch?: never trace?: never } - '/volumes/{volumeID}': { + '/secrets/{secretID}': { parameters: { query?: never header?: never @@ -2431,61 +2562,111 @@ export interface paths { cookie?: never } /** - * Team volume - * @description Get team volume info + * Get a secret + * @description Get one secret's metadata, selected by identifier or name. */ get: { parameters: { query?: never header?: never path: { - volumeID: components['parameters']['volumeID'] + secretID: components['parameters']['secretID'] } cookie?: never } requestBody?: never responses: { - /** @description Successfully retrieved a team volume */ + /** @description Successfully retrieved the secret */ 200: { headers: { [name: string]: unknown } content: { - 'application/json': components['schemas']['VolumeAndToken'] + 'application/json': components['schemas']['Secret'] } } + 400: components['responses']['400'] 401: components['responses']['401'] + 403: components['responses']['403'] 404: components['responses']['404'] + 409: components['responses']['409'] + 429: components['responses']['429'] 500: components['responses']['500'] + 502: components['responses']['502'] + 504: components['responses']['504'] } } put?: never - post?: never /** - * Delete team volume - * @description Delete a team volume + * Update a secret + * @description Replace the secret's stored marker by appending a new version. The response carries metadata only. + */ + post: { + parameters: { + query?: never + header?: never + path: { + secretID: components['parameters']['secretID'] + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['SecretUpdate'] + } + } + responses: { + /** @description Successfully updated the secret */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Secret'] + } + } + 400: components['responses']['400'] + 401: components['responses']['401'] + 403: components['responses']['403'] + 404: components['responses']['404'] + 409: components['responses']['409'] + 429: components['responses']['429'] + 500: components['responses']['500'] + 502: components['responses']['502'] + 504: components['responses']['504'] + } + } + /** + * Delete a secret + * @description Revoke the secret and schedule its versions for cleanup. */ delete: { parameters: { query?: never header?: never path: { - volumeID: components['parameters']['volumeID'] + secretID: components['parameters']['secretID'] } cookie?: never } requestBody?: never responses: { - /** @description Successfully deleted a team volume */ + /** @description Successfully deleted the secret */ 204: { headers: { [name: string]: unknown } content?: never } + 400: components['responses']['400'] 401: components['responses']['401'] + 403: components['responses']['403'] 404: components['responses']['404'] + 409: components['responses']['409'] + 429: components['responses']['429'] 500: components['responses']['500'] + 502: components['responses']['502'] + 504: components['responses']['504'] } } options?: never @@ -2553,6 +2734,12 @@ export interface components { * @enum {string} */ SandboxState: 'running' | 'paused' + /** + * @description Sort direction + * @default desc + * @enum {string} + */ + OrderDirection: 'asc' | 'desc' SnapshotInfo: { /** @description Identifier of the snapshot template including the tag. Uses namespace/alias when a name was provided (e.g. team-slug/my-snapshot:default), otherwise falls back to the raw template ID (e.g. abc123:default). */ snapshotID: string @@ -2879,6 +3066,8 @@ export interface components { * @description Automatically pauses the sandbox after the timeout */ autoPause?: boolean + /** @description Defaults to true. When false, resume from disk state only: the sandbox cold-boots fresh and any memory in the snapshot is ignored, never modified or deleted. Disk state has crash-recovery semantics — writes not flushed before the pause may be lost. A no-op for snapshots that contain no memory. Rejected with an error in environments where this capability is not enabled, never silently downgraded to a memory restore. */ + memory?: boolean } ConnectSandbox: { /** @@ -2886,6 +3075,8 @@ export interface components { * @description Timeout in seconds from the current time after which the sandbox should expire */ timeout: number + /** @description Defaults to true. When false and the sandbox is paused, resume from disk state only: the sandbox cold-boots fresh and any memory in the snapshot is ignored, never modified or deleted. Disk state has crash-recovery semantics — writes not flushed before the pause may be lost. A no-op for snapshots that contain no memory. Rejected with an error in environments where this capability is not enabled, never silently downgraded to a memory restore. */ + memory?: boolean } SandboxTimeoutRequest: { /** @@ -2974,6 +3165,14 @@ export interface components { /** @description Number of sandboxes that failed to kill */ failedCount: number } + /** + * @description Cached live sandbox index count keyed by team ID. Counts may briefly + * include sandboxes transitioning out of running; teams without indexed + * sandboxes are omitted. + */ + AdminTeamRunningSandboxCounts: { + [key: string]: number + } AdminBuildCancelResult: { /** @description Number of builds successfully cancelled */ cancelledCount: number @@ -3508,8 +3707,6 @@ export interface components { */ sandboxCount: number metrics: components['schemas']['NodeMetrics'] - /** @description List of cached builds id on the node */ - cachedBuilds: string[] /** * Format: uint64 * @description Number of sandbox create successes @@ -3521,27 +3718,6 @@ export interface components { */ createFails: number } - CreatedAccessToken: { - /** - * Format: uuid - * @description Identifier of the access token - */ - id: string - /** @description Name of the access token */ - name: string - /** @description The fully created access token */ - token: string - mask: components['schemas']['IdentifierMaskingDetails'] - /** - * Format: date-time - * @description Timestamp of access token creation - */ - createdAt: string - } - NewAccessToken: { - /** @description Name of the access token */ - name: string - } TeamAPIKey: { /** * Format: uuid @@ -3635,6 +3811,8 @@ export interface components { * @description Error code */ code: number + /** @description Machine-readable semantic error code. Not a closed set; initial values: sandbox_capacity_unavailable, sandbox_placement_timeout, sandbox_no_compatible_node, sandbox_create_failed, internal_server_error. */ + error_code?: string /** @description Error */ message: string } @@ -3661,11 +3839,57 @@ export interface components { name: string /** @description Auth token to use for interacting with volume content */ token: string + /** + * @description Domain to use as the destination for volume content requests, + * replacing the default `api.`. Only returned when the + * team is connected to a custom (BYOC) cluster; absent otherwise, in + * which case the default domain is used. + */ + domain?: string } NewVolume: { /** @description Name of the volume */ name: string } + /** @description Customer metadata of the secret. Always present, empty when unset. At most 32 entries; keys are limited to 128 bytes, values to 1024 bytes, and a secret's metadata to 8192 bytes in total. */ + SecretMetadata: { + [key: string]: string + } + /** @description Metadata of a secret. It never carries the secret value. */ + Secret: { + /** @description Identifier of the secret */ + secretID: string + /** @description Name of the secret, unique within the project */ + name: string + /** + * Format: int64 + * @description Version served to readers that do not name one + */ + currentVersion: number + metadata: components['schemas']['SecretMetadata'] + /** + * Format: date-time + * @description Time when the secret was created + */ + createdAt: string + /** + * Format: date-time + * @description Time when the secret was last updated + */ + updatedAt: string + } + NewSecret: { + /** @description Name of the secret, unique within the project. Names are lower-cased before storage and returned in that canonical form; the sec_ prefix is reserved for secret identifiers. */ + name: string + /** @description Runtime marker stored as the secret's first version. The runtime resolves it to a value at sandbox egress. */ + value: string + metadata?: components['schemas']['SecretMetadata'] + } + SecretUpdate: { + /** @description Runtime marker stored as the secret's new version. The runtime resolves it to a value at sandbox egress. */ + value: string + metadata?: components['schemas']['SecretMetadata'] + } } responses: { /** @description Bad request */ @@ -3713,8 +3937,8 @@ export interface components { 'application/json': components['schemas']['Error'] } } - /** @description Gone */ - 410: { + /** @description Too many requests */ + 429: { headers: { [name: string]: unknown } @@ -3731,6 +3955,33 @@ export interface components { 'application/json': components['schemas']['Error'] } } + /** @description Backend error */ + 502: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Service unavailable */ + 503: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } + /** @description Backend timeout */ + 504: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Error'] + } + } } parameters: { templateID: string @@ -3739,7 +3990,6 @@ export interface components { teamID: string nodeID: string apiKeyID: string - accessTokenID: string snapshotID: string tag: string /** @description Maximum number of items to return per page */ @@ -3747,9 +3997,15 @@ export interface components { /** @description Cursor to start the list from */ paginationNextToken: string volumeID: string + secretID: string } requestBodies: never - headers: never + headers: { + /** @description Cursor to fetch the next page of results, if more exist */ + XNextToken: string + /** @description Number of running sandboxes matching the filters, before pagination is applied. Only present when running sandboxes were requested. */ + XTotalRunning: number + } pathItems: never } export type $defs = Record