{
  "openapi": "3.1.0",
  "info": {
    "title": "Assure key rotation",
    "version": "1",
    "description": "The key rotation route only (issue #1264). From 7 days before a key expires (day 23 of a 30-day key, or the last quarter of a shorter operator-issued key's life; a GitHub sign-in key never rotates) the key's holder asks for its successor; the old key keeps working until its own expiry, so a rotation never fails a task in flight. The successor is minted once and answered again, the same, inside the overlap; only the key's SHA-256 is kept, and the sealed copy that answers a second rotate is readable only with the old key."
  },
  "servers": [
    {
      "url": "https://api.symbolia.ai"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "paths": {
    "/v1/keys/rotate": {
      "post": {
        "operationId": "rotateKey",
        "description": "The Authorization header's key is the credential and the subject. No body (Content-Length 0) and no query parameters. From the key's rotation window (Assure-Key-Expires minus 7 days, or the last quarter of its life) the answer is a new key for the same account and plan, expiring 30 days from now, with predecessor naming this key; inside the overlap a second rotate with the old key answers the same successor with reissued true. Before the window: 409 rotate_too_early, detail.opens_at the time it opens (null for a key with no expiry yet). Counts against the account's minute rate window.",
        "x-body-max-bytes": 0,
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The successor key; the answer also carries the presented key's Assure-Key-Expires",
            "headers": {
              "Assure-Key-Expires": {
                "description": "The presented key's expiry",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Rotated"
                }
              }
            }
          },
          "400": {
            "description": "bad_input: a body or a query parameter was sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated: the key is missing, malformed, unknown or revoked; key_expired: the key is past its expiry (detail.expires)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "405": {
            "description": "bad_input: method not allowed",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "409": {
            "description": "rotate_too_early: before the key's rotation window; detail.opens_at names when it opens and the key keeps working",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: the account's minute window is full",
            "headers": {
              "Retry-After": {
                "description": "Seconds until another attempt may be made",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "Flags": {
        "type": "object",
        "required": [
          "execution_authorized",
          "hardware_authorized",
          "industrial_release_authorized",
          "release_allowed",
          "physical_validation",
          "simulation",
          "self_approved"
        ],
        "properties": {
          "execution_authorized": {
            "type": "boolean",
            "const": false
          },
          "hardware_authorized": {
            "type": "boolean",
            "const": false
          },
          "industrial_release_authorized": {
            "type": "boolean",
            "const": false
          },
          "release_allowed": {
            "type": "boolean",
            "const": false
          },
          "physical_validation": {
            "type": "boolean",
            "const": false
          },
          "simulation": {
            "type": "boolean",
            "const": true
          },
          "self_approved": {
            "type": "boolean",
            "const": false
          }
        }
      },
      "Rotated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Flags"
          },
          {
            "type": "object",
            "required": [
              "schema",
              "key_id",
              "key",
              "expires_at",
              "account_id",
              "predecessor",
              "reissued"
            ],
            "properties": {
              "schema": {
                "type": "string",
                "const": "assure.serve.keys.rotate/v1"
              },
              "key_id": {
                "type": "string",
                "pattern": "^[0-9a-f]{8}$"
              },
              "key": {
                "type": "string",
                "description": "The successor key, asr_<key_id>_<secret>; shown here and never stored"
              },
              "expires_at": {
                "type": "string",
                "description": "ISO time, UTC, 30 days from the first rotate"
              },
              "account_id": {
                "type": "string"
              },
              "predecessor": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "key_id",
                  "expires_at"
                ],
                "properties": {
                  "key_id": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string",
                    "description": "When the presented key stops working"
                  }
                }
              },
              "reissued": {
                "type": "boolean",
                "description": "true when this successor was minted by an earlier rotate inside the overlap"
              }
            }
          }
        ],
        "unevaluatedProperties": false
      },
      "Failure": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Flags"
          },
          {
            "type": "object",
            "required": [
              "schema",
              "check_id",
              "outcome",
              "reason",
              "action",
              "detail",
              "charged"
            ],
            "properties": {
              "schema": {
                "type": "string",
                "const": "assure.serve.failure/v1"
              },
              "check_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "request_id": {
                "type": "string",
                "pattern": "^[0-9a-f]{16}$",
                "description": "The X-Request-Id of the request whose refusal was answered before a check was admitted (issue #1101); every refusal on this route is answered before any check, so check_id is null and request_id is present (REFUTATION-r3 H2)."
              },
              "outcome": {
                "type": "string",
                "enum": [
                  "bad_input",
                  "unauthenticated",
                  "key_expired",
                  "rotate_too_early",
                  "rate_limited",
                  "checker_error"
                ]
              },
              "reason": {
                "type": "string"
              },
              "action": {
                "type": "string"
              },
              "detail": {
                "type": "object",
                "properties": {
                  "served_from": {
                    "type": "string",
                    "const": "handler",
                    "description": "Issue #1665: every refusal on this route is minted while answering this request, so it is handler"
                  },
                  "opens_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "rotate_too_early: when the window opens; null for a key with no expiry yet, and for a GitHub sign-in key, which never rotates (the reason says which)"
                  },
                  "expires": {
                    "type": "string",
                    "description": "rotate_too_early and key_expired: the presented key's expiry"
                  },
                  "window": {
                    "type": "string",
                    "enum": [
                      "minute",
                      "day"
                    ]
                  },
                  "retry_after_s": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "malformation": {
                    "type": "object"
                  },
                  "allowed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "405: the methods the route takes"
                  },
                  "route": {
                    "type": "string",
                    "description": "405: the route template"
                  }
                }
              },
              "charged": {
                "type": "boolean",
                "const": false,
                "description": "No check is admitted by the rotation route; every refusal is uncharged."
              },
              "build": {
                "type": "object",
                "required": [
                  "commit",
                  "release_sha256",
                  "checkers_sha256"
                ],
                "properties": {
                  "commit": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "release_sha256": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "checkers_sha256": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "Issue #1100: every freshly API-built refusal carries the serving engine's identity, including checker_error that refuses a development verdict; never copy the refused development block, kind or tree_sha256. Exactly commit, release_sha256 and checkers_sha256, the three keys of a verdict's engine block; all three are null if the engine pin fails verification. Compare build.commit first. Historical stored and non-API failures can omit this optional outer property and remain as originally written.",
                "additionalProperties": false
              }
            }
          }
        ],
        "unevaluatedProperties": false
      }
    }
  }
}
