{ "openapi": "3.1.0", "info": { "title": "WAI deployment API", "version": "1", "description": "The API a deployment target serves (extensions/deployment.md §4): files put by their SHA-256, a deployment held by its digest (the SHA-256 of its build record, dist/wai-build.json), the live deployment promoted and rolled back, each answer to a list, a create, a promotion and a rollback carrying a receipt signed by the target's key (§5). Any host that serves these routes under an address is a deployment target; the paths are under that address. Each operationId names the operation of the WAI toolchain that sends the request, where one does.", "license": { "name": "Apache-2.0", "identifier": "Apache-2.0" } }, "paths": { "/v1/deployments": { "get": { "operationId": "deployment.list", "summary": "What the target holds: each deployment, the live one and every one made live, with a signed receipt", "parameters": [ { "name": "nonce", "in": "query", "required": true, "description": "Repeated in the receipt: 32 to 128 lowercase hexadecimal digits, fresh for each request.", "schema": { "$ref": "#/components/schemas/Nonce" } } ], "responses": { "200": { "description": "The deployments held, and the receipt whose state is them", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/List" } } } }, "400": { "description": "A nonce that is not one (`invalid-request`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "operationId": "deployment.create", "summary": "A deployment held by its digest, once the target holds its build record and every file the record lists", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "digest", "nonce" ], "properties": { "digest": { "$ref": "#/components/schemas/Digest" }, "nonce": { "$ref": "#/components/schemas/Nonce" } } } } } }, "responses": { "200": { "description": "Already held: the receipt, the target unchanged", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Created" } } } }, "201": { "description": "Held now: the receipt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Created" } } } }, "400": { "description": "A body that is not a create (`invalid-request`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Files it does not hold, each to be put by its SHA-256; the record itself is named by the path wai-build.json (`files-missing`)", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "required": [ "missing" ], "properties": { "missing": { "type": "array", "items": { "$ref": "#/components/schemas/Missing" } } } } ] } } } }, "422": { "description": "A build record that is not a deployment's (§3.3), or a file it lists whose size is another (`tree-refused`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/deployments/files/{sha256}": { "put": { "operationId": "deployment_file.create", "summary": "A file held by its SHA-256: the bytes sent must hash to it", "parameters": [ { "name": "sha256", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Digest" } } ], "requestBody": { "required": true, "description": "The file's bytes, with Content-Length.", "content": { "application/octet-stream": { "schema": { "type": "string", "contentMediaType": "application/octet-stream" } } } }, "responses": { "200": { "description": "Already held", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FileHeld" } } } }, "201": { "description": "Held now", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FileHeld" } } } }, "400": { "description": "A path that is not a SHA-256 (`invalid-request`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "411": { "description": "A body without Content-Length (`length-required`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "Larger than the target takes (its list's limits.file_bytes) (`too-large`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "Bytes that are not the SHA-256 named; nothing is kept (`digest-mismatch`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/deployments/promote": { "post": { "operationId": "deployment.promote", "summary": "A deployment the target holds made its live one", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "digest", "nonce" ], "properties": { "digest": { "$ref": "#/components/schemas/Digest" }, "nonce": { "$ref": "#/components/schemas/Nonce" }, "expect_live": { "description": "Where given, the request is refused unless this is the live deployment (null: none is live).", "oneOf": [ { "$ref": "#/components/schemas/Digest" }, { "type": "null" } ] } } } } } }, "responses": { "200": { "description": "Live: the receipt (changed false when it was live already, and nothing changed)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Changed" } } } }, "400": { "description": "A body that is not a promotion (`invalid-request`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "A digest the target does not hold (`deployment-not-held`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "expect_live is not the live deployment (`live-changed`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/deployments/rollback": { "post": { "operationId": "deployment.rollback", "summary": "The deployment live before the live one made live again", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "nonce" ], "properties": { "nonce": { "$ref": "#/components/schemas/Nonce" }, "expect_live": { "description": "Where given, the request is refused unless this is the live deployment.", "oneOf": [ { "$ref": "#/components/schemas/Digest" }, { "type": "null" } ] } } } } } }, "responses": { "200": { "description": "Rolled back: the receipt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Changed" } } } }, "400": { "description": "A body that is not a rollback (`invalid-request`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "No deployment was live before the live one, or expect_live is not the live deployment (`nothing-to-roll-back`, `live-changed`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "Digest": { "description": "A SHA-256, lowercase hexadecimal.", "type": "string", "pattern": "^[0-9a-f]{64}$" }, "Nonce": { "type": "string", "pattern": "^[0-9a-f]{32,128}$" }, "Error": { "type": "object", "required": [ "error", "message" ], "properties": { "error": { "enum": [ "invalid-request", "not-found", "deployment-not-held", "method-not-allowed", "files-missing", "live-changed", "nothing-to-roll-back", "length-required", "too-large", "digest-mismatch", "tree-refused", "internal" ] }, "message": { "type": "string" } }, "description": "A refusal (§4.6): its code, and what happened." }, "Missing": { "type": "object", "required": [ "path", "sha256", "bytes" ], "properties": { "path": { "type": "string" }, "sha256": { "$ref": "#/components/schemas/Digest" }, "bytes": { "type": [ "integer", "null" ], "minimum": 0 } } }, "FileHeld": { "type": "object", "required": [ "sha256", "bytes", "held" ], "properties": { "sha256": { "$ref": "#/components/schemas/Digest" }, "bytes": { "type": "integer", "minimum": 0 }, "held": { "const": true } } }, "Held": { "type": "object", "required": [ "digest", "project", "files", "bytes" ], "properties": { "digest": { "$ref": "#/components/schemas/Digest" }, "project": { "type": "object", "required": [ "sha256" ], "properties": { "name": { "type": "string" }, "sha256": { "$ref": "#/components/schemas/Digest" } } }, "files": { "type": "integer", "minimum": 0 }, "bytes": { "type": "integer", "minimum": 0 } } }, "List": { "type": "object", "required": [ "format", "live", "history", "held", "serves", "limits", "receipt" ], "properties": { "format": { "const": "wai.deployments/1" }, "live": { "oneOf": [ { "$ref": "#/components/schemas/Digest" }, { "type": "null" } ] }, "history": { "type": "array", "items": { "$ref": "#/components/schemas/Digest" }, "description": "Every deployment made live and not rolled back, oldest first; the last is the live one." }, "held": { "type": "array", "items": { "$ref": "#/components/schemas/Held" }, "description": "Every deployment held, sorted by digest." }, "serves": { "type": [ "string", "null" ], "description": "The path under the target's address at which it serves its live deployment, or null where it serves none." }, "limits": { "type": "object", "required": [ "file_bytes" ], "properties": { "file_bytes": { "type": "integer", "minimum": 0 } } }, "receipt": { "$ref": "#/components/schemas/Receipt" } } }, "Created": { "type": "object", "required": [ "created", "receipt" ], "properties": { "created": { "type": "boolean" }, "receipt": { "$ref": "#/components/schemas/Receipt" } } }, "Changed": { "type": "object", "required": [ "changed", "receipt" ], "properties": { "changed": { "type": "boolean" }, "receipt": { "$ref": "#/components/schemas/Receipt" } } }, "Receipt": { "type": "object", "description": "Signed by the target (§5): Ed25519 by key over the RFC 8785 form of every member but signature.", "additionalProperties": false, "required": [ "format", "op", "key", "nonce", "seq", "digest", "live", "previous", "state", "time", "signature" ], "properties": { "format": { "const": "wai.deployment-receipt/1" }, "op": { "enum": [ "deployment.list", "deployment.create", "deployment.promote", "deployment.rollback" ] }, "key": { "description": "The target's Ed25519 public key.", "type": "string", "pattern": "^[0-9a-f]{64}$" }, "nonce": { "$ref": "#/components/schemas/Nonce" }, "seq": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, "description": "How many changes the target has made, this one included." }, "digest": { "description": "The deployment the request named; for a rollback, the one made live; null for a list.", "oneOf": [ { "$ref": "#/components/schemas/Digest" }, { "type": "null" } ] }, "live": { "description": "The live deployment after the request.", "oneOf": [ { "$ref": "#/components/schemas/Digest" }, { "type": "null" } ] }, "previous": { "description": "The live deployment before it.", "oneOf": [ { "$ref": "#/components/schemas/Digest" }, { "type": "null" } ] }, "state": { "description": "The SHA-256 of the RFC 8785 form of {\"held\": [every digest held, sorted], \"history\": [every one made live, oldest first]} after the request.", "type": "string", "pattern": "^[0-9a-f]{64}$" }, "time": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" }, "signature": { "type": "string", "pattern": "^[0-9a-f]{128}$" } } } } } }