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, 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--3.automerge" X-Continuum-Swarm: description: The swarm ID. schema: type: string X-Continuum-Epoch: description: '`:`.' 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. Secrets are left out however they were given (`secret`). Text with placeholders (`${env:NAME}`) is shown as `values` says; `templates` lists the text as written. parameters: - name: values in: query required: false description: | Text with placeholders as it ends up (`interpolated`), or as written (`original`). schema: type: string enum: [interpolated, original] default: interpolated 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 templates: type: array items: type: string secret: type: boolean 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: 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-.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 (`:`). 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: object description: By public key. additionalProperties: $ref: '#/components/schemas/Peer' Peer: type: object required: [status, handshakeComplete, hasPendingDelivery, neighbour, traffic, incidents] properties: 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, largestReceivedBytes] properties: lastFromNeighbourSecs: type: [integer, 'null'] divergedPeers: type: integer largestReceivedBytes: 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: , rules}`. `import`: `{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: '`:`.' 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 (`:`). 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