# WAI Extension: Deployment > Status: Draft. A **deployment** is a project's built tree: `dist/` as `wai build` writes it, its > files and its build record, `wai-build.json`, named by its **digest**, the SHA-256 of the build > record's bytes. A **deployment target** is a host that holds deployments by their digests and > keeps one of them **live**. This extension states what a deployment is and what a target serves > of it (§3), the API every target serves (§4), the receipt a target signs for each answer (§5), > how a target holds its deployments (§6), the reference target (§7) and the toolchain's > operations (§8). It names no host: any host that serves §4 under an address is a target. The > API's OpenAPI document is [`deployment.openapi.json`](deployment.openapi.json). The reference > implementation is the module `deployment` of `wai-rs` (feature `ed25519`), the target > `wai_deploy_target` and the operations of `wai_cli/dx/deploy.rs`; `wai-rs/tests/cli_deploy.rs` > holds the three, and the OpenAPI document, to each other. > Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. > > Texts read for this revision: the WAI texts this one cites; RFC 8032; RFC 8259; RFC 8785; > RFC 9110; FIPS 180-4; OpenAPI 3.1.0. ## 1. Scope In scope: the tree a deployment is, its digest, the rules it is held to and what a target serves of it (§3); the routes of the API, their requests, answers and refusals (§4); the receipt, its members, its signature and what a client holds it to (§5); how a target holds files, deployments and its live one (§6); the reference target (§7); and the toolchain's operations (§8). Out of scope: who may change a target, which is decided in front of it (a request carries a key as every request of the toolchain does, `wai auth login`); names, certificates and caches in front of a target; a build on the target, which holds what was built and builds nothing; and removing a deployment, which this revision does not define. ## 2. Terms - **Tree**: the files of `dist/` and its build record. - **Build record**: `dist/wai-build.json`, as [`schema/README.md`](../schema/README.md) states it: the project's and the lock's digests, the toolchain, and every other file of the tree with its path, SHA-256 and size. - **Digest**: the SHA-256 of the build record's bytes, 64 lowercase hexadecimal digits. Two builds of one project and lock give one record, so one digest. - **Held**: a file is held by a target when it keeps its bytes under their SHA-256; a deployment is held when its build record and every file the record lists are held, and the target has taken it (§4.3). - **Live**: the deployment a target serves. **History**: every deployment made live and not rolled back, oldest first; its last is the live one. - **Change**: a create that holds a deployment the target did not hold, a promotion that changes the live deployment, or a rollback. A target counts its changes, **seq**. ## 3. A deployment ### 3.1 The tree A deployment is the build record and every file it lists, each at its path, nothing else. A tree with a file the record does not list is not the record's tree, and a client **MUST NOT** send it (§8.1); a name of a file or directory that starts with a dot is not part of a tree, as `wai build` writes none and `wai verify` reads none. ### 3.2 The digest The digest is the SHA-256 of the build record's bytes as `wai build` wrote them: RFC 8785 over one object, on one line, with no line ending. The record names every file by its SHA-256, so the digest names every byte of the tree. ### 3.3 The rules a tree is held to A client before it sends a tree, and a target before it holds one, **MUST** refuse a build record that is not 1. the RFC 8785 form of what it says, byte for byte; 2. an object with exactly the members `$schema` (`https://wai.transaction.science/schema/wai-build.json/1`), `wai` (`1`), `project` (`sha256`, and `name` where it has one), `lock` (`sha256` alone), `toolchain` (an object) and `files`; 3. a `files` list of objects of exactly `path`, `sha256` (64 lowercase hexadecimal digits) and `bytes` (a whole number of at most 2^53 − 1), sorted by path as bytes, each path once; 4. with every path 1 to 1024 bytes of `/`-joined segments, none empty, `.` or `..`, none holding a backslash or a control character, none `wai-build.json`, and no path a directory of another (`a` beside `a/b`). A client **MUST** also hold each file of `dist/` to the record: its SHA-256 and size those the record states. A target **MUST** hold each file it is sent to the SHA-256 it is put at (§4.2), and a file a record lists to the size the record states (§4.3). ### 3.4 What a target serves A target that serves its live deployment serves each file of the tree at its path under the path its list names as `serves` (§4.1), byte for byte, and the build record at `wai-build.json`, so that anyone reading it can hold the tree to it. A request for that path itself, or for a path that ends with `/`, is the `index.html` there; a path that names no file of the tree, but whose `/index.html` is one, is that file; any other path is 404. Each file's media type is read from its name's extension. A target **MAY** hold deployments and serve none, and then lists `serves` as `null`. ## 4. The API Every path below is under the target's address, which may itself have a path. A request and an answer with a body are JSON (RFC 8259), but a file's bytes (§4.2). A request names a **nonce**, 32 to 128 lowercase hexadecimal digits, fresh for each request; the receipt repeats it (§5). A body is sent with `Content-Length`. | Request | Operation | Does | |---|---|---| | `GET /v1/deployments?nonce=` | `deployment.list` | what the target holds, signed (§4.1) | | `PUT /v1/deployments/files/{sha256}` | `deployment_file.create` | a file held by its SHA-256 (§4.2) | | `POST /v1/deployments` | `deployment.create` | a deployment held by its digest (§4.3) | | `POST /v1/deployments/promote` | `deployment.promote` | a deployment held made live (§4.4) | | `POST /v1/deployments/rollback` | `deployment.rollback` | the one live before the live one made live again (§4.5) | ### 4.1 List `GET /v1/deployments?nonce=` answers 200 with `format` (`wai.deployments/1`); `live`, the live deployment's digest or `null`; `history`; `held`, every deployment held, sorted by digest, each with its `digest`, its `project` (`sha256`, and `name` where its record names one), how many `files` and how many `bytes`; `serves` (§3.4); `limits.file_bytes`, the largest file it takes; and `receipt` (§5), whose `state` is the digest of the `held` and `history` listed. It changes nothing. ### 4.2 A file `PUT /v1/deployments/files/{sha256}` with a file's bytes (`application/octet-stream`) holds them under `{sha256}`. Bytes that do not hash to it are refused, `digest-mismatch` (422), and nothing is kept. It answers 201 when the file is new and 200 when it was held, each with `sha256`, `bytes` and `held: true`. A file once held is never changed. A record is put the same way, under its digest. ### 4.3 Create `POST /v1/deployments` with `{"digest", "nonce"}`. The target reads the file it holds under the digest as a build record (§3.3) and checks that it holds every file the record lists, by SHA-256 and size. When it does, it holds the deployment and answers 201 with `created: true` and the receipt. A deployment already held is the same deployment: 200, `created: false`, and nothing changes. When it lacks files, 409 `files-missing` with `missing`: each file it lacks, by `path`, `sha256` and `bytes`, the record itself by the path `wai-build.json` and `bytes: null`. A record that does not hold to §3.3, or a file whose size is not the one the record states, is refused, `tree-refused` (422). A create never changes which deployment is live. ### 4.4 Promote `POST /v1/deployments/promote` with `{"digest", "nonce"}`, and `expect_live` where the client read the live deployment first. A digest the target does not hold is refused, `deployment-not-held` (404), and nothing changes; an `expect_live` that is not the live deployment (`null`: none is live) is refused, `live-changed` (409). Otherwise the deployment is appended to the history and is live: 200, `changed: true`, and the receipt. Promoting the live deployment changes nothing: 200, `changed: false`. ### 4.5 Rollback `POST /v1/deployments/rollback` with `{"nonce"}`, and `expect_live` as for a promotion. The last deployment of the history is removed from it, and the one before it is live: exactly that one, whatever was promoted since. A history of fewer than two deployments is refused, `nothing-to-roll-back` (409), and nothing changes. 200, `changed: true`, and the receipt, whose `digest` and `live` are the deployment made live and whose `previous` is the one rolled back. ### 4.6 Refusals A refusal answers `{"error": , "message": }`, and the members its code names: | Code | Status | Raised by | |---|---|---| | `invalid-request` | 400 | a body, a nonce, a digest or a SHA-256 that is not one | | `not-found` | 404 | a path under `/v1/deployments/` that is no route | | `deployment-not-held` | 404 | a promotion of a digest the target does not hold | | `method-not-allowed` | 405 | a route asked with another method | | `files-missing` | 409 | a create whose record or files are not all held; `missing` | | `live-changed` | 409 | an `expect_live` that is not the live deployment; `live` | | `nothing-to-roll-back` | 409 | a rollback with no deployment before the live one | | `length-required` | 411 | a body without `Content-Length` | | `too-large` | 413 | a body larger than the target takes; `limit` | | `digest-mismatch` | 422 | a file whose bytes are not the SHA-256 it is put at; `sha256`, theirs | | `tree-refused` | 422 | a record that does not hold to §3.3, or a size it states that a held file does not have | | `internal` | 500 | a target that could not keep what it was sent; nothing changed | ## 5. The receipt Every answer to a list, a create, a promotion and a rollback carries `receipt`, an object of exactly these members: | Member | Holds | |---|---| | `format` | `wai.deployment-receipt/1` | | `op` | the operation it answers: `deployment.list`, `deployment.create`, `deployment.promote` or `deployment.rollback` | | `key` | the target's Ed25519 public key (RFC 8032), 64 lowercase hexadecimal digits | | `nonce` | the request's nonce | | `seq` | the target's count of changes, this one included | | `digest` | the deployment the request named; for a rollback, the one made live; `null` for a list | | `live` | the live deployment after the request, or `null` | | `previous` | the live deployment before it, or `null`; `live` itself where the request did not change it | | `state` | the SHA-256 of the RFC 8785 form of `{"held": [...], "history": [...]}` after the request, every digest held sorted, and the history oldest first | | `time` | the target's clock, `YYYY-MM-DDTHH:MM:SSZ` | | `signature` | the Ed25519 signature by `key` over the RFC 8785 form of every other member, 128 lowercase hexadecimal digits | A client **MUST** refuse an answer whose receipt does not verify, whose `op` is not the request's, whose `nonce` is not the one it sent, or that is signed by another key than the target's other answers in the same operation. It **MUST** also hold what the receipt states to the request: a list's `held` and `history` to its `state`, and its `live` to the history's last; a create's `digest` to the deployment sent, and its `live` to its `previous`; a promotion's `digest` and `live` to the deployment named, and its `previous` to the live deployment the client read; and a rollback's `live` and `digest` to the deployment before the live one in the history the client read, and its `previous` to that live one. A refused receipt means the client does not know what the target did, and says so. ## 6. Holding ### 6.1 Files and deployments A target keeps each file by its SHA-256 and never changes it, so a file several deployments list is held once. It holds a deployment from its create until this revision's end: no request removes one. ### 6.2 The live deployment and its history A promotion appends to the history, a rollback removes its last, and nothing else changes it. Each change is whole: a target keeps its state so that a crash leaves the state before the change or the state after it, and answers only after the state after it is kept. A request that is refused, or that changes nothing, leaves `seq` as it was. ### 6.3 Serving A target that serves its live deployment serves it as §3.4 states, from the files it holds, so the bytes served are the bytes the record names. ## 7. The reference target `wai_deploy_target --dir [--port P] [--bind ADDR] [--key ] [--max-file-bytes N]` serves §4 over the directory `D`, which it owns: `files/` each written whole and renamed into place, and `state.json`, the deployments held, the history and `seq`, written whole in place of the one before. Its key is the Ed25519 seed `--key` names (64 hexadecimal digits), else `D/target.key`, made on its first start, readable by its owner alone. On start it reads its state back and holds every deployment it names to its record and files again, by SHA-256, and does not start when one does not hold. It listens on `127.0.0.1` unless `--bind` names another address, on `--port` (0, the default, picks a free one), prints one JSON line when it is ready (`ready`, `url`, `key`, `dir`), answers one request at a time, serves its live deployment at `/` (`serves: "/"`), and takes files of at most `--max-file-bytes` (256 MiB). It has no accounts: who may reach it is decided in front of it. ## 8. The operations The toolchain's operations, each named by its rule (an operation `noun.verb`, its command line the words in order), each in a build with the feature `quickstart`: | Operation | Command line | Requests | Effect | |---|---|---|---| | `deployment.create` | `wai deploy [] [--target ] [--prebuilt] [--promote] [--key ]` | list; create; each file it lacks; create; with `--promote`, promote | the target | | `deployment.list` | `wai deployment list [--target ]` | list | reads | | `deployment.promote` | `wai deployment promote [--target ] [--key ]` | list; promote | the target | | `deployment.rollback` | `wai deployment rollback [--target ] [--key ]` | list; rollback | the target | ### 8.1 `wai deploy` `wai deploy` builds the project in `` (`.`) as `wai build` does, its checks and its receipt, and refuses as it refuses; with `--prebuilt` it takes `dist/` as it is. Either way it holds the tree to its record (§3.3) before it sends anything: a file the record lists that is not there, or whose SHA-256 or size is another, or a file there the record does not list, is refused (exit 1, `dist-changed`), and nothing is sent. It then reads the target's list and asks the target to hold the deployment by its digest; for each answer `files-missing` it puts what the target names, from `dist/`, each held to its SHA-256 again, and asks again (the record first, where the target lacks it, then the files the record lists). With `--promote` it then promotes the deployment, naming as `expect_live` the live one its create's receipt states. A file the target already holds is not sent. ### 8.2 Every operation `--target` names the target as every operation that acts on a service names one: a target's name (`wai target add`) or the target's address; each takes the target in use (`wai target use`) when none is named. A read-only target refuses each that changes it. A request carries the key `wai auth login` kept for the target's origin, or `WAI_TOKEN`'s, and only over `https://` or to this machine. Each that changes the target waits for `--yes` (`--confirm`, deprecated, is the same) before it builds, writes or sends anything, and without it exits 4 with the proposed change and the command that confirms it. `--dry-run` sends nothing, not even a list, and writes nothing: it names each request it would send and each file it would write. `--json` writes the operation's JSON envelope. Each verifies every receipt it is answered with and holds it to its request (§5); a receipt that does not hold is refused (exit 1, `receipt-refused`). Each that changes the target writes each receipt of a change to `.wai/deployments//-.json` under `` (for a promotion and a rollback, the current directory), `` the first 16 hexadecimal digits of the SHA-256 of the target's address and `` the first 16 of the request's, and then its own operation receipt ([`schema/README.md`](../schema/README.md), *Operation receipts*), naming each as written and, for `wai deploy`, the build record as read; `--key` signs it. `wai verify` holds the receipts' files to it. A refusal by the target is the operation's, by its code: `deployment-not-held` exits 5; `live-changed`, `nothing-to-roll-back`, `digest-mismatch`, `tree-refused` and `files-missing` exit 1; a target that cannot be reached exits 6. ## 9. Conformance A target conforms when it serves §4 and signs §5 as stated, holds as §6 states, and refuses with the codes of §4.6. A client conforms when it holds a tree to §3.3 before sending it and every receipt to §5. `wai-rs/tests/cli_deploy.rs` holds the reference target and the toolchain's operations to this text against each other, plants the faults this text refuses (a file of `dist/` changed after its build, bytes put under another SHA-256, a promotion of a deployment the target does not hold, a rollback with none before the live one, a receipt answering another request) and requires each to be refused, requires a rollback to make live exactly the deployment live before, requires `--dry-run` to leave the target as it was, and holds the OpenAPI document's routes, operations, codes and receipt members to the implementation's. ## 10. Security considerations A digest names every byte of a tree, so a target cannot serve, under a deployment's digest, bytes the record does not name, and a client that holds its tree to its record sends no byte the build did not write. A receipt binds a target's key to what it did for one request, by its nonce, so an answer to another request, or a receipt kept from before, is refused; it does not say who may hold that key, which is the target's operator's to publish. A target is reached as every service of the toolchain is (`--target`), and a key goes only over `https://` or to this machine. The reference target listens on loopback and has no accounts: a target reached from elsewhere is put behind what decides who may reach it.