diff --git a/openapi/control-v1.yaml b/openapi/control-v1.yaml index 4d9eae9..dca4ecf 100644 --- a/openapi/control-v1.yaml +++ b/openapi/control-v1.yaml @@ -308,6 +308,20 @@ paths: application/json: schema: $ref: ../schemas/control-v1.schema.json#/$defs/ConnectionManifest + '202': + description: >- + Confirmed over-cap reconnect creates or returns a durable `session.display_relaunch` termination operation. + Under the session and unique Stop lock, the Server MUST create at most one display-relaunch operation per session; + a same-owner/device lost-response retry MUST return that same operation. It reuses the existing one-way non-reissued + Stop work/ack path. An existing user Stop in any state MUST win and yield a stable non-202 result; it MUST NOT be reinterpreted as display-relaunch authority. This response is not a manifest, + does not assert termination completion, and does not authorize a replacement session before `applied`. + After `applied`, the client submits a fresh `SessionRequest` with a new idempotency key. + Failed or `termination_unconfirmed` outcomes never auto-relaunch. Local Stop or teardown MUST suppress the fresh launch. + Maximum JSON body: 16384 bytes. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/StopOperation '400': {$ref: '#/components/responses/InvalidRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} diff --git a/tools/validate.py b/tools/validate.py index 325e0a2..2beec0e 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -319,6 +319,21 @@ def main() -> int: for operation_id in ("logoutSession", "requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession"): operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0] assert " browserCsrfHeader: []\n - nativeBearer: []\n" in operation, f"{operation_id}: native bearer must remain a separate OR requirement" + reconnect_endpoint = openapi.split(" operationId: reconnectBrokerSession\n", 1)[1].split("\n /api/", 1)[0] + assert " '202':\n" in reconnect_endpoint + relaunch_response = reconnect_endpoint.split(" '202':\n", 1)[1].split(" '400':", 1)[0] + assert "$defs/StopOperation" in relaunch_response + assert "durable `session.display_relaunch` termination operation" in relaunch_response + assert "MUST create at most one display-relaunch operation per session" in relaunch_response + assert "same-owner/device lost-response retry MUST return that same operation" in relaunch_response + assert "existing user Stop in any state MUST win and yield a stable non-202 result" in relaunch_response + assert "MUST NOT be reinterpreted as display-relaunch authority" in relaunch_response + assert "not a manifest" in relaunch_response + assert "does not assert termination completion" in relaunch_response + assert "does not authorize a replacement session before `applied`" in relaunch_response + assert "fresh `SessionRequest` with a new idempotency key" in relaunch_response + assert "Failed or `termination_unconfirmed` outcomes never auto-relaunch" in relaunch_response + assert "Local Stop or teardown MUST suppress the fresh launch" in relaunch_response for operation_id in ("loginBrowserSession", "rotateNativeCredential", "issueNativeTunnelCredential"): operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0] assert "browserCsrf" not in operation, f"{operation_id}: excluded operation gained browser CSRF"