diff options
Diffstat (limited to 'docs/openapi.yaml')
| -rw-r--r-- | docs/openapi.yaml | 1697 |
1 files changed, 1697 insertions, 0 deletions
diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..f81ed4c --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,1697 @@ +openapi: 3.1.0 +info: + title: continuum + version: 0.1.0 + description: | + HTTP API of a continuum node: a replicated Automerge key-value store that syncs + over Mycelium. + + Every endpoint requires HTTP basic authentication (users from `--htpasswd`, + Argon2 hashes), except `/health`, `/ready`, the Swagger UI (`/swagger-ui`) and + this description (`/openapi.yaml`). + `/debug/*` exists only with `--debug-api`. + + Written by hand, and served by every node as it is (`/openapi.yaml`). + + Conventions: + * Store paths are slash-separated keys; empty segments are invalid. Keys + containing `/` can't be addressed through the path. + * Timestamps in remake and quarantine records are Unix milliseconds. + * Change hashes and epoch IDs are lowercase hex (64 characters). + * Request bodies are limited to 2 MiB, except `POST /admin/remakes` (64 MiB). + +servers: + - url: http://127.0.0.1:3000 + +security: + - basic: [] + +tags: + - name: Store + description: Reading and writing the replicated document. + - name: Peers + description: Other nodes this node knows. + - name: Admin + description: Node operation (log level, statistics, document export). + - name: Remakes + description: | + Replacing the swarm's document with a new epoch (compaction, replacement, + import), and what happened to local writes that a remake couldn't place. + - name: Debug + description: Test hooks, only with `--debug-api`. + +paths: + /store: + get: + tags: [Store] + summary: Read the whole store + description: | + The merged view of the document as JSON (an empty object if empty), or with + `Accept: text/plain` as text. Includes protocol data under `_continuum`. + + While the node rejoins a newer epoch, reads are served from the previous + document, marked with `x-continuum-stale`. + parameters: + - $ref: '#/components/parameters/conflicts' + - $ref: '#/components/parameters/heads' + responses: + '200': + $ref: '#/components/responses/Value' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Reading or serializing failed. + '503': + description: Node is not ready (and has no stale view to serve). + + /store/{key}: + parameters: + - $ref: '#/components/parameters/key' + get: + tags: [Store] + summary: Read a value + description: | + The value at the path, as JSON, or with `Accept: text/plain` as text (strings + unquoted, anything else as JSON). Concurrent writes resolve as follows: + concurrent maps merge, anything else keeps the newest write. + parameters: + - $ref: '#/components/parameters/conflicts' + - $ref: '#/components/parameters/heads' + responses: + '200': + $ref: '#/components/responses/Value' + '400': + description: Invalid path. + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: No value at this path. + '500': + description: Reading or serializing failed. + '503': + description: Node is not ready (and has no stale view to serve). + put: + tags: [Store] + summary: Write a value + description: | + Writes the value, creating intermediate maps. One request is one Automerge + change. Answers with the value written (JSON, or text with + `Accept: text/plain`). + + JSON maps and arrays become Automerge maps and lists; numbers become integers + when they fit, floats otherwise; strings are plain strings (not text objects). + requestBody: + required: true + content: + application/json: + schema: {} + example: { "theme": "dark", "fontSize": 14 } + text/plain: + schema: + type: string + example: dark + responses: + '200': + $ref: '#/components/responses/Value' + '400': + description: Invalid path, malformed JSON or invalid UTF-8. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Protocol data (`_continuum`) and the user isn't in `protocol-writers`, or epoch metadata or the swarm ID (`_continuum/epochs`, `_continuum/swarm`, never writable). + '409': + description: The path crosses an existing non-map value. + '415': + description: Content-Type is neither `application/json` nor `text/plain` (absent means JSON). + '500': + description: Writing failed. + '503': + description: Node is not ready. + delete: + tags: [Store] + summary: Delete a value + parameters: + - name: prune + in: query + description: Also remove ancestor maps left empty. + schema: + type: boolean + default: false + responses: + '204': + description: Deleted. + '400': + description: Invalid path. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Protocol data (`_continuum`) and the user isn't in `protocol-writers`, or epoch metadata or the swarm ID (`_continuum/epochs`, `_continuum/swarm`, never writable). + '404': + description: No value at this path. + '500': + description: Deleting failed. + '503': + description: Node is not ready. + + /stream: + get: + tags: [Store] + summary: Follow the whole store + description: | + As `/stream/{key}`, for the store root (as `GET /store` reads it): one event + with the whole store, then one whenever anything in it changes. Events have an + empty `key`. + responses: + '200': + description: Event stream. + content: + application/x-ndjson: + schema: + $ref: '#/components/schemas/StreamEvent' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/Error' + '503': + $ref: '#/components/responses/Error' + + /stream/{key}: + parameters: + - $ref: '#/components/parameters/key' + get: + tags: [Store] + summary: Follow a value + description: | + Newline-delimited JSON: one event with the current value, then one whenever + the resolved value changes (by a local write or by sync). The connection stays + open. + responses: + '200': + description: Event stream. + content: + application/x-ndjson: + schema: + $ref: '#/components/schemas/StreamEvent' + example: | + {"key":"settings/theme","value":"dark"} + {"key":"settings/theme","deleted":true} + '400': + description: Invalid path. + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Reading the initial value failed. + '503': + description: Node is not ready. + + /peers: + get: + tags: [Peers] + summary: List known peers + responses: + '200': + description: Peers, sorted by public key. + content: + application/json: + schema: + $ref: '#/components/schemas/PeerList' + '401': + $ref: '#/components/responses/Unauthorized' + + /peers/{peer}: + delete: + tags: [Peers] + summary: Forget a peer swarm-wide + description: | + Writes a forget record into the document and removes the peer from the member + list, so every node drops it. A forgotten node that is still alive registers + again on its next message. + parameters: + - name: peer + in: path + required: true + description: The peer's public key. + schema: + type: string + - name: force + in: query + description: '`true` or `alive`: also forget a peer that is currently alive.' + schema: + type: string + responses: + '204': + description: Forget recorded. + '400': + description: Empty key, this node's own key, or an unknown `force`. + '401': + $ref: '#/components/responses/Unauthorized' + '409': + description: The peer is alive; pass `force=alive`. + '500': + description: Writing the record failed. + '503': + description: Node is not ready. + + /admin/log: + get: + tags: [Admin] + summary: Current log filter + responses: + '200': + description: The filter in `RUST_LOG` syntax. + content: + text/plain: + schema: + type: string + example: info,continuum::swarm::sync=debug + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: The filter can't be read. + put: + tags: [Admin] + summary: Replace the log filter + description: Takes effect immediately; not kept across restarts. + requestBody: + required: true + content: + text/plain: + schema: + type: string + example: info,continuum::swarm::sync=trace + responses: + '200': + description: The filter now in effect. + content: + text/plain: + schema: + type: string + '400': + description: Not UTF-8, or not a valid filter (the reason is in `error`); the old filter stays. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + + /health: + get: + tags: [Admin] + summary: Liveness + description: The HTTP API is up. Unauthenticated. + security: [] + responses: + '200': + description: Up. + content: + text/plain: + schema: + type: string + example: "ok\n" + + /ready: + get: + tags: [Admin] + summary: Readiness + description: | + The node serves reads and writes: `200` when ready, `503` while joining or + rejoining. Unauthenticated. + security: [] + responses: + '200': + description: Ready. + content: + text/plain: + schema: + type: string + example: "ready\n" + '503': + description: Not ready (joining or rejoining). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /admin/stats: + get: + tags: [Admin] + summary: Node statistics + description: | + JSON by default. When `Accept` names `application/openmetrics-text` or + `text/plain` (as Prometheus does), the Prometheus text format (0.0.4) instead, + with per-peer series labelled `peer` (the public key). Aliases are only in + `continuum_info` and `continuum_peer_info`, for joining. + responses: + '200': + description: Statistics. + content: + application/json: + schema: + $ref: '#/components/schemas/Stats' + text/plain; version=0.0.4: + schema: + type: string + example: | + # HELP continuum_uptime_seconds Seconds since this node started. + # TYPE continuum_uptime_seconds gauge + continuum_uptime_seconds 3600 + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/document: + get: + tags: [Admin] + summary: Export the document + description: | + The whole local document as saved by Automerge (history and `_continuum` + included): a backup, and input to an `import` remake. + responses: + '200': + description: The saved document. + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename="continuum-<swarm>-3.automerge" + X-Continuum-Swarm: + description: The swarm ID. + schema: + type: string + X-Continuum-Epoch: + description: '`<generation>:<epoch id hex>`.' + schema: + type: string + X-Continuum-Heads: + description: The document's heads (hex, comma-separated). + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '429': + description: Another export is in progress (one at a time; each holds copies of the whole document). + '500': + description: Saving failed. + '503': + description: Node is not ready. + + /admin/config: + get: + tags: [Admin] + summary: Effective configuration + description: | + Every option with its value and where it came from (`default`, `file` with the + file, or `argument`), as `--print-config` prints it. Imported values (`env:`, + `file:`, `cmd:`) are left out; `imported` lists their importers. + responses: + '200': + description: The configuration. + content: + application/json: + schema: + type: object + required: [files, options] + properties: + files: + type: array + items: + type: string + options: + type: object + additionalProperties: + type: object + required: [source] + properties: + source: + type: string + enum: [default, file, argument] + file: + type: string + imported: + type: array + items: + type: string + value: {} + example: + files: [/etc/continuum.toml] + options: + neighbours: { source: default, value: 12 } + alias: { source: argument, imported: ['env:HOSTNAME'] } + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/remakes: + get: + tags: [Remakes] + summary: List remakes + description: All remake records in the local document, oldest proposal first. + responses: + '200': + description: Remakes. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + post: + tags: [Remakes] + summary: Propose a remake + description: | + Proposes a remake of the current epoch; this node coordinates it and votes for + it. The electorate is fixed now: the members that renewed their alive stamp + within this node's `--electorate-window`. Each member votes for at most + one remake of its epoch (again only once that one is aborted). The coordinator commits as soon as a majority of + the electorate voted for it, and aborts once that is out of reach or + `settle` passed without it. Every node switches to the new epoch after the + switch delay. Writes made after the base are carried over (translated by the + kind's rules; what no rule covers is quarantined on the node that made it). + + An `import` can also be uploaded as the raw document with + `Content-Type: application/octet-stream`; its options then go in the query + (`rules`, `baseHeads`, `settle`, `switchDelay`). Those query parameters + are refused with a JSON body. + parameters: + - $ref: '#/components/parameters/wait' + - $ref: '#/components/parameters/remakeForce' + - name: rules + in: query + description: 'Octet-stream import only: the rules as JSON, e.g. `[{"from":"","to":""}]`.' + schema: + type: string + - name: baseHeads + in: query + description: Octet-stream import only, comma-separated hex. + schema: + type: string + - name: settle + in: query + description: Octet-stream import only; a duration (seconds, or e.g. `500ms`, `8m`). + schema: + type: string + - name: switchDelay + in: query + description: Octet-stream import only; a duration. + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeRequest' + examples: + compact: + value: {} + replace: + value: + kind: replace + spec: + document: { "accounts": {} } + rules: + - { "from": "users", "to": "accounts" } + - { "from": "tmp", "drop": true } + restore: + summary: Import a backup, keeping later writes at their paths + value: + kind: import + spec: + document: <base64> + rules: + - { "from": "", "to": "" } + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: With `wait=true`, the final report. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Proposed. + headers: + Location: + schema: + type: string + example: /admin/remakes/0f8e… + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '400': + $ref: '#/components/responses/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: With `wait=true`, the record vanished. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: | + Another remake of this epoch is still open (`remake` holds its ID); an + expired one still holds votes or can't be superseded yet (`expired` lists + each with `id`, `coordinator`, `coordinatorActive`, `expiredAt`, + `abortableAt` and `tally`; `force` names the override, if any); or this + node can't vote for a new one: it voted for another remake of this epoch, + or may have before it restarted. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '413': + description: Request larger than 64 MiB. + '500': + $ref: '#/components/responses/Error' + '503': + $ref: '#/components/responses/Error' + delete: + tags: [Remakes] + summary: Abort the remake in progress + description: | + Aborts whichever remake of the local epoch is still undecided (the oldest if + several), as `DELETE /admin/remakes/{id}` does. + parameters: + - $ref: '#/components/parameters/wait' + - name: force + in: query + description: 'As for `DELETE /admin/remakes/{id}`: `true`, or a comma-separated list of `coordinatorActive` and `coordinatorInactive`.' + schema: + type: string + responses: + '200': + description: Aborted (or decided otherwise meanwhile). + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Abort requested; the coordinator applies it within about a second. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + $ref: '#/components/responses/AlreadyDecided' + '500': + $ref: '#/components/responses/Error' + + /admin/remakes/{id}: + parameters: + - $ref: '#/components/parameters/remakeId' + get: + tags: [Remakes] + summary: Report one remake + parameters: + - name: wait + in: query + description: Long-poll until decided (bounded by the proposal's expiry, unless adopted). + schema: + type: boolean + default: false + responses: + '200': + description: The report. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + delete: + tags: [Remakes] + summary: Abort a remake + description: | + Only while undecided; a committed remake can't be undone. An undecided one + whose coordinator this node doesn't reach answers `409` (the coordinator + could commit first), unless `force=coordinatorInactive`. An expired remake + is aborted by this node (`200`), as its coordinator is gone, once a grace + period has passed since it expired (`409` with `abortableAt` before), and, + if this node sees its coordinator as an active peer, only with + `force=coordinatorActive`. + parameters: + - $ref: '#/components/parameters/wait' + - name: force + in: query + description: '`true`, or a comma-separated list of `coordinatorActive` (abort an expired remake whose coordinator this node sees as an active peer) and `coordinatorInactive` (request the abort of an undecided remake whose coordinator this node doesn''t reach).' + schema: + type: string + responses: + '200': + description: Aborted (or decided otherwise meanwhile). + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Abort requested. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + $ref: '#/components/responses/AlreadyDecided' + '500': + $ref: '#/components/responses/Error' + + /admin/remakes/{id}/adopt: + parameters: + - $ref: '#/components/parameters/remakeId' + post: + tags: [Remakes] + summary: Adopt a remake + description: | + This node becomes the coordinator of an expired remake (its coordinator is + gone), or of one adopted before. An adopted remake has no voting period: it + commits once a majority of its electorate voted for it, and aborts once that + is out of reach or on request. Adopt on one node only. + parameters: + - $ref: '#/components/parameters/wait' + responses: + '200': + description: With `wait=true`, the final report. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '202': + description: Adopted. + content: + application/json: + schema: + $ref: '#/components/schemas/RemakeReport' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + description: | + Still in its voting period, already decided, or not a remake of this + node's epoch. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/Error' + '503': + $ref: '#/components/responses/Error' + + /admin/epochs: + get: + tags: [Remakes] + summary: Epochs on this node + responses: + '200': + description: The local epoch, archived ones, and a catch-up with a newer epoch in progress. + content: + application/json: + schema: + $ref: '#/components/schemas/Epochs' + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/quarantine: + get: + tags: [Remakes] + summary: List quarantined writes + description: | + This node's own writes that a remake had no place for, kept in memory on this + node only. + responses: + '200': + description: Entries. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/QuarantineSummary' + '401': + $ref: '#/components/responses/Unauthorized' + + /admin/quarantine/{id}: + parameters: + - $ref: '#/components/parameters/quarantineId' + get: + tags: [Remakes] + summary: Show quarantined writes + responses: + '200': + description: The entry and every path it holds, with the value the old document has there. + content: + application/json: + schema: + type: object + required: [entry, writes] + properties: + entry: + $ref: '#/components/schemas/QuarantineSummary' + writes: + type: array + items: + $ref: '#/components/schemas/QuarantinedWrite' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + delete: + tags: [Remakes] + summary: Discard quarantined writes + responses: + '204': + description: Discarded. + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + + /admin/quarantine/{id}/document: + parameters: + - $ref: '#/components/parameters/quarantineId' + get: + tags: [Remakes] + summary: Download the old document + responses: + '200': + description: The old epoch's document, as saved by Automerge. + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename="quarantine-<id>.automerge" + content: + application/octet-stream: + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + + /admin/quarantine/{id}/replay: + parameters: + - $ref: '#/components/parameters/quarantineId' + post: + tags: [Remakes] + summary: Replay quarantined writes + description: | + Writes them into the current epoch at their old paths, as fresh writes (they + win over what is there), and removes the entry. If none can be written, the + entry stays. + responses: + '200': + description: Replayed. + content: + application/json: + schema: + type: object + required: [replayed, failed] + properties: + replayed: + type: integer + failed: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/Error' + '409': + description: None of the writes could be replayed (`failed` holds the count). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + $ref: '#/components/responses/Error' + + /debug/firewall: + get: + tags: [Debug] + summary: Current firewall rules + responses: + '200': + description: Rules. + content: + application/json: + schema: + $ref: '#/components/schemas/FirewallRules' + '401': + $ref: '#/components/responses/Unauthorized' + put: + tags: [Debug] + summary: Replace the firewall rules + description: Drops messages from (`inbound`) or to (`outbound`) these peers, like a lossy network. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FirewallRules' + responses: + '204': + description: Replaced. + '401': + $ref: '#/components/responses/Unauthorized' + '422': + description: Invalid body. + +components: + securitySchemes: + basic: + type: http + scheme: basic + + parameters: + key: + name: key + in: path + required: true + description: Slash-separated store path, e.g. `settings/theme`. + schema: + type: string + conflicts: + name: conflicts + in: query + description: | + Answer `{"value": …, "conflicts": [...]}` (always JSON), listing every + conflict resolved on the way. + schema: + type: boolean + default: false + heads: + name: heads + in: query + description: Return the document's heads at the time of the read in `x-continuum-heads` (e.g. as the base of a remake). + schema: + type: boolean + default: false + wait: + name: wait + in: query + description: Wait until the remake is decided and answer with the final report. + schema: + type: boolean + default: false + remakeForce: + name: force + in: query + description: | + Checks to override: `true` for all, or a comma-separated list of + `switchDelay` (accept one below a keepalive interval; tests only) and + `supersede` (abort expired remakes of this epoch that still hold votes, and + propose in their place) and `coordinatorActive` (supersede even though this + node sees their coordinator as an active peer). + schema: + type: string + examples: + all: + value: 'true' + supersede: + value: supersede + remakeId: + name: id + in: path + required: true + schema: + type: string + format: uuid + quarantineId: + name: id + in: path + required: true + schema: + type: string + format: uuid + + responses: + Unauthorized: + description: Missing or wrong credentials. + headers: + WWW-Authenticate: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Value: + description: The value. + headers: + x-continuum-heads: + description: With `heads=true`, the heads at the time of the read (sorted hex, comma-separated). + schema: + type: string + x-continuum-stale: + description: Served from the previous epoch's document while rejoining; the value is that epoch (`<generation>:<id prefix>`). + schema: + type: string + ETag: + description: SHA-256 of the body. + schema: + type: string + content: + application/json: + schema: + oneOf: + - description: The value. + - $ref: '#/components/schemas/ValueWithConflicts' + text/plain: + schema: + type: string + Error: + description: Error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + AlreadyDecided: + description: Already committed (can't be undone) or already over; `remake` holds the report. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + schemas: + Error: + type: object + required: [error] + properties: + error: + type: string + additionalProperties: true + description: >- + The body of every error answer (4xx and 5xx), on every endpoint. `error` is a + human-readable message; some errors add fields (`remake`, `failed`, `force`). + + StreamEvent: + type: object + required: [key] + properties: + key: + type: string + value: + description: The current value; absent when deleted. + deleted: + type: boolean + const: true + + ValueWithConflicts: + type: object + required: [value, conflicts] + properties: + value: {} + conflicts: + type: array + items: + $ref: '#/components/schemas/Conflict' + + Conflict: + type: object + required: [path, resolution, candidates] + properties: + path: + type: array + items: + type: string + ancestor: + type: boolean + description: On the way to the requested path (candidates without values). + resolution: + type: string + enum: [mergedMaps, newestWins] + candidates: + type: array + items: + type: object + required: [opId, selected] + properties: + opId: + type: string + selected: + type: boolean + value: {} + + Epoch: + type: object + required: [generation, id] + properties: + generation: + type: integer + minimum: 0 + id: + type: string + description: Hex snapshot hash; all zeros for the genesis epoch. + + PeerList: + type: object + required: [localEpoch, localHeads, peers] + properties: + localAlias: + type: [string, 'null'] + localEpoch: + $ref: '#/components/schemas/Epoch' + localHeads: + type: integer + description: 'Heads of the local document (more than one: concurrent changes not yet merged).' + peers: + type: array + items: + $ref: '#/components/schemas/Peer' + + Peer: + type: object + required: [publicKey, status, handshakeComplete, hasPendingDelivery, neighbour, traffic, incidents] + properties: + publicKey: + type: string + alias: + type: [string, 'null'] + description: The name the peer gave itself (`--alias`); not unique. + validatedSwarm: + type: [string, 'null'] + status: + type: string + enum: [active, dormant] + handshakeComplete: + type: boolean + lastSeenSecsAgo: + type: [integer, 'null'] + dormantForSecs: + type: [integer, 'null'] + hasPendingDelivery: + type: boolean + epoch: + oneOf: + - $ref: '#/components/schemas/Epoch' + - type: 'null' + sameEpoch: + type: [boolean, 'null'] + heads: + type: [integer, 'null'] + description: Heads in its last keepalive; null before one or when it sent a digest (more than 32). + neighbour: + type: boolean + description: We keep it alive and sync with it on our own; other peers we only answer. + rttMs: + type: [number, 'null'] + description: Effective round-trip time (smoothed, raised by unanswered requests); null without a recent sample. + traffic: + $ref: '#/components/schemas/Traffic' + incidents: + $ref: '#/components/schemas/Incidents' + syncThroughput: + oneOf: + - $ref: '#/components/schemas/Throughput' + - type: 'null' + + Counter: + type: object + required: [messages, bytes] + properties: + messages: + type: integer + bytes: + type: integer + description: Payload bytes (without Mycelium framing). + + ByKind: + type: object + required: [hello, helloReply, sync, ping, pong, blob] + properties: + hello: + $ref: '#/components/schemas/Counter' + helloReply: + $ref: '#/components/schemas/Counter' + sync: + $ref: '#/components/schemas/Counter' + ping: + $ref: '#/components/schemas/Counter' + pong: + $ref: '#/components/schemas/Counter' + blob: + $ref: '#/components/schemas/Counter' + description: Blob requests and answers (chunked downloads, e.g. of an epoch's snapshot). + + Traffic: + type: object + description: Since start. Every PONG answers a PING, so received pongs over sent pings is the keepalive answer ratio. + required: [sent, received, refused] + properties: + sent: + $ref: '#/components/schemas/ByKind' + received: + $ref: '#/components/schemas/ByKind' + refused: + $ref: '#/components/schemas/ByKind' + description: Sends Mycelium refused (not counted as sent). + + Incidents: + type: object + required: [replyTimeouts, heals, dormancies, dormantMs] + properties: + replyTimeouts: + type: integer + description: Our sync messages unanswered until the reply timeout (then resent). + heals: + type: integer + description: Re-handshakes because documents differed while sync was idle. + dormancies: + type: integer + dormantMs: + type: integer + description: Time spent dormant up to the last return (the current dormancy isn't included). + + Throughput: + type: object + required: [bytesPerSec, samples, secsAgo] + properties: + bytesPerSec: + type: number + description: Smoothed, from sync exchanges of at least 32 KiB. + samples: + type: integer + secsAgo: + type: integer + description: Age of the last sample. + + Stats: + type: object + required: [publicKey, uptimeSecs, ready, epoch, document, catchUp, remakes, sync, relay, topology, stability, staleHelloReplies, traffic, incidents, propagation, disk] + properties: + publicKey: + type: string + alias: + type: [string, 'null'] + uptimeSecs: + type: integer + ready: + type: boolean + epoch: + $ref: '#/components/schemas/Epoch' + document: + type: object + required: [heads, maxHeads] + properties: + heads: + type: integer + maxHeads: + type: integer + description: Most heads after applying a sync message, since start. + savedBytes: + type: [integer, 'null'] + description: Size as saved (compressed), measured in the background; null until first measured. + savedBytesSecsAgo: + type: [integer, 'null'] + savedBytesTookMs: + type: [integer, 'null'] + catchUp: + type: object + required: [sources] + properties: + sources: + type: integer + description: Catch-up sources chosen (more than one means it switched). + doneAfterSecs: + type: [number, 'null'] + description: From start until the initial sync was done; null until then and on the genesis node. + syncBytes: + type: [integer, 'null'] + remakes: + type: object + description: Remakes and their aftermath here (`docs/measurements.md`, "Remakes"). + required: [undecided, committed, rejoining, catchUpAttempts, quarantined, archivedEpochs] + properties: + undecided: + type: integer + committed: + type: integer + description: More than one is a double commit. + rejoining: + type: boolean + catchUpAttempts: + type: integer + quarantined: + type: integer + oldestQuarantineSecs: + type: [integer, 'null'] + archivedEpochs: + type: integer + sync: + type: object + required: [divergedPeers] + properties: + lastFromNeighbourSecs: + type: [integer, 'null'] + divergedPeers: + type: integer + relay: + type: object + required: [announceRounds, pulls, pullsQueued, blobRequestsDropped] + properties: + announceRounds: + type: integer + pulls: + type: integer + pullsQueued: + type: integer + blobRequestsDropped: + type: integer + topology: + type: object + required: [k] + properties: + k: + type: integer + view: + oneOf: + - type: 'null' + - type: object + required: [members, reachable, stable, neighbours, stableNeighbours, fullMesh] + properties: + members: + type: integer + description: Registered members, this node included. + reachable: + type: integer + description: Members other than this node not judged unreachable. + stable: + type: integer + neighbours: + type: integer + stableNeighbours: + type: integer + fullMesh: + type: boolean + stability: + type: object + required: [tier, connectedFraction, observedSecs] + properties: + tier: + type: string + enum: [stable, transient] + connectedFraction: + type: number + description: Fraction of the observed time (up to a day) with a live peer. + observedSecs: + type: integer + staleHelloReplies: + type: integer + disk: + type: object + description: | + This node's own files, by kind (`state`: the state file, `document`: the + document file, `backup`: a backup recovered from, `remake`: the input of a + genesis remake), since it started. + additionalProperties: + type: object + required: [reads, readBytes, writes, writtenBytes, appends, appendedBytes, errors] + properties: + reads: + type: integer + readBytes: + type: integer + writes: + type: integer + writtenBytes: + type: integer + description: Whole files written. + appends: + type: integer + appendedBytes: + type: integer + errors: + type: integer + description: Failed reads and writes (a missing file is no failure). + traffic: + $ref: '#/components/schemas/Traffic' + description: All peers, including ones no longer listed. + incidents: + $ref: '#/components/schemas/Incidents' + description: The peers currently listed. + propagation: + type: object + description: Time from a change's commit elsewhere until applied here, once caught up (includes clock skew). + required: [count, sumMs, ahead, buckets] + properties: + count: + type: integer + sumMs: + type: integer + ahead: + type: integer + description: Changes stamped later than our clock on arrival. + buckets: + type: array + description: Not cumulative; the last bucket has no bound. + items: + type: object + required: [count] + properties: + leMs: + type: [integer, 'null'] + count: + type: integer + + Retention: + type: object + description: Kept until every given limit has passed (as reports show it). + properties: + epochs: + type: integer + ageMs: + type: integer + + RetentionRequest: + type: object + description: Kept until every given limit has passed (as a request gives it). + properties: + epochs: + type: integer + age: + oneOf: [{ type: string }, { type: number }] + description: A duration (seconds, or e.g. `7d`, `12h`). + + Rule: + type: object + description: | + Where writes made after the base go. Paths are slash-separated strings (`""` + is the root and matches everything) or arrays of segments. The longest + matching `from` wins; the rest of the path is appended to `to`. Exactly one of + `to` and `drop`. Paths not covered are quarantined. + required: [from] + properties: + from: + $ref: '#/components/schemas/RulePath' + to: + $ref: '#/components/schemas/RulePath' + drop: + type: boolean + + RulePath: + oneOf: + - type: string + - type: array + items: + type: string + + RemakeRequest: + type: object + additionalProperties: false + properties: + kind: + type: string + enum: [compact, replace, import] + default: compact + spec: + description: | + `compact`: none. `replace`: `{document: <store root object>, rules}`. + `import`: `{document: <base64 of an Automerge document>, rules}`; its + `_continuum` and history are left out, Automerge types are kept. + oneOf: + - type: 'null' + - type: object + required: [document] + additionalProperties: false + properties: + document: + oneOf: + - type: object + - type: string + contentEncoding: base64 + rules: + type: array + items: + $ref: '#/components/schemas/Rule' + baseHeads: + type: array + items: + type: string + description: | + Heads (hex) whose content the new document accounts for; the current heads + if absent. Not for `compact`, whose base is the coordinator's heads when it + commits (`400`). + replayWindow: + $ref: '#/components/schemas/RetentionRequest' + description: 'How stale a returning node''s writes may be and still be replayed. Default: 1 epoch, 7 days.' + archiveRetention: + $ref: '#/components/schemas/RetentionRequest' + description: 'How long nodes keep the old document. Default: 2 epochs, 30 days.' + settle: + oneOf: [{ type: string }, { type: number }] + description: How long votes are collected, a duration (seconds, or e.g. `500ms`, `8m`); default twice the dormancy threshold. + switchDelay: + oneOf: [{ type: string }, { type: number }] + description: A duration; default one keepalive interval, at least that without `force=switchDelay`. + + RemakeReport: + type: object + required: [id, phase, proposal] + properties: + id: + type: string + format: uuid + phase: + type: string + enum: [proposed, expired, committed, aborted] + proposal: + type: object + required: [coordinator, fromEpoch, policy, settleMs, proposedAt, settleUntil, electorate, quorum, kind] + properties: + coordinator: + type: string + fromEpoch: + $ref: '#/components/schemas/Epoch' + baseHeads: + type: array + description: Absent for `compact`; its base is in the decision. + items: + type: string + policy: + type: object + properties: + replayWindow: + $ref: '#/components/schemas/Retention' + archiveRetention: + $ref: '#/components/schemas/Retention' + settleMs: + type: integer + proposedAt: + type: integer + settleUntil: + type: integer + switchDelayMs: + type: integer + electorate: + type: array + description: Members whose votes count, fixed at proposal time. + items: + type: string + quorum: + type: integer + description: Votes needed to commit (a majority of the electorate). + kind: + type: string + content: + type: object + description: The new document of a replace or import remake, by reference; it stays on the coordinator and every voter downloads it. + required: [blake3, size] + properties: + blake3: + type: string + description: BLAKE3 hash of the content, hex. + size: + type: integer + translation: + description: The kind's rules, as stored. + decision: + oneOf: + - type: object + required: [outcome, toEpoch, snapshotBytes, decidedAt] + properties: + outcome: + const: committed + toEpoch: + $ref: '#/components/schemas/Epoch' + baseHeads: + type: array + description: The base fixed at the commit (`compact`). + items: + type: string + snapshotBytes: + type: integer + decidedAt: + type: integer + - type: object + required: [outcome, reason, decidedAt] + properties: + outcome: + const: aborted + reason: + type: string + decidedAt: + type: integer + abortRequested: + type: object + properties: + by: + type: string + at: + type: integer + adoptedBy: + type: object + description: The node that took over coordinating, if any. + properties: + by: + type: string + at: + type: integer + contested: + type: boolean + description: | + Present (true) when decisions written concurrently disagree; `decision` is + then the commit, and nodes don't switch to it on their own. + votes: + type: object + description: | + Votes for this remake by member public key, from the votes register (only + electorate members count; one that also voted for another remake counts + for neither). + additionalProperties: + type: object + required: [remake, at] + properties: + remake: + type: string + at: + type: integer + local: + type: object + description: What happened to this remake on the answering node. + properties: + switchedAt: + type: integer + replayedPaths: + type: integer + failedPaths: + type: integer + quarantine: + type: string + description: ID of the quarantine entry with this node's writes that weren't replayed. + error: + type: string + description: Why the switch didn't happen (yet). + + Epochs: + type: object + required: [current, archived, quarantine] + properties: + current: + $ref: '#/components/schemas/Epoch' + archived: + type: array + items: + type: object + required: [generation, id, archivedAt] + properties: + generation: + type: integer + id: + type: string + archivedAt: + type: integer + catchUp: + oneOf: + - type: 'null' + - type: object + required: [target, attempts, rejoining] + properties: + target: + type: string + description: '`<generation>:<id prefix>`.' + attempts: + type: integer + rejoining: + type: boolean + description: Syncing the target epoch (reads are served stale meanwhile). + error: + type: string + quarantine: + type: array + description: IDs of quarantine entries. + items: + type: string + + QuarantineSummary: + type: object + required: [id, epoch, target, createdAt, reason, paths] + properties: + id: + type: string + epoch: + type: string + description: The epoch the writes were made in (`<generation>:<id prefix>`). + target: + type: string + description: The epoch the node rejoined. + createdAt: + type: integer + reason: + type: string + paths: + type: integer + + QuarantinedWrite: + type: object + required: [path] + properties: + path: + type: array + items: + type: string + value: + description: The old document's value there. + deleted: + type: boolean + const: true + error: + type: string + + FirewallRules: + type: object + properties: + inbound: + type: array + items: + type: string + uniqueItems: true + outbound: + type: array + items: + type: string + uniqueItems: true |
