diff --git a/content/docs/(reference)/api-reference/index.mdx b/content/docs/(reference)/api-reference/index.mdx index ee3c65f8..6b92d167 100644 --- a/content/docs/(reference)/api-reference/index.mdx +++ b/content/docs/(reference)/api-reference/index.mdx @@ -63,6 +63,7 @@ The reference groups operations by what they act on. The groups and the operatio | Event stream | Changes to items and edges, streamed as they happen | [Stream change events](/api-reference/event-stream/streamEvents) | | Webhooks | Changes sent to a URL you choose, and each delivery's record | [Create a webhook](/api-reference/webhooks/createWebhook) | | Folders | A folder's settings, held as a `system.folder` item | [Create a folder](/api-reference/folders/createFolder) | +| Views | Saved lists of items and how each app shows them, held as `system.view` items | [Create a view](/api-reference/views/createView) | | Connectors | Processes outside the server that read and write on your behalf, with their heartbeats and runs | [Register a connector](/api-reference/connectors/registerConnector) | | Connector state | What the instance keeps for a connector's source: its hold, its state and its agreements | [Take a connector hold](/api-reference/connector-state/takeConnectorHold) | | Inbound webhooks | Addresses where a service sends requests to a connector, and the deliveries Marfa stores for it | [Create an inbound endpoint](/api-reference/inbound-webhooks/createInboundEndpoint) | diff --git a/content/docs/catalog/(system)/meta.json b/content/docs/catalog/(system)/meta.json index 205ca9e9..37844792 100644 --- a/content/docs/catalog/(system)/meta.json +++ b/content/docs/catalog/(system)/meta.json @@ -3,6 +3,7 @@ "defaultOpen": false, "pages": [ "connection", - "folder" + "folder", + "view" ] } diff --git a/content/docs/catalog/(system)/view.mdx b/content/docs/catalog/(system)/view.mdx new file mode 100644 index 00000000..78e176c7 --- /dev/null +++ b/content/docs/catalog/(system)/view.mdx @@ -0,0 +1,9 @@ +--- +title: View +description: "A saved list of items, held here and shared by every app: which items, and how each layout shows them. Written only through /views: created, changed at a version, and deleted, which removes it at once with no trash. Lifecycle bounded to active | revoked. Has no tier." +identifier: system.view +--- + +[Save a list as a view](/guides/list-filter-search#save-a-list-as-a-view) uses the view operations that write these settings. No item operation writes a view: each one refuses a `system.*` type with `403 type_not_permitted` ([`items/system-types`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemssystem-types)). + +src/generated/catalog/system.view.md diff --git a/content/docs/guides/list-filter-search.mdx b/content/docs/guides/list-filter-search.mdx index 61a33685..008c4334 100644 --- a/content/docs/guides/list-filter-search.mdx +++ b/content/docs/guides/list-filter-search.mdx @@ -229,6 +229,50 @@ Each entry has a `starts_at`, an `ends_at` where the event has one, and the `ite The whole window comes in one page, in order of `starts_at`. A window spans at most **400 days**, and Marfa refuses one with too many occurrences rather than trim it, so narrow the window. If the answer carries `expansion_incomplete: true`, **the calendar is missing something**: `series_errors` names the series that couldn't be unfolded. +## Save a list as a view + +A [view](/catalog/view) saves a listing so every app shows the same one: its `query` says which items it lists, and its `layouts` say how each one shows them. [Create a view](/api-reference/views/createView) with a `title`, a `query` of `types`, `tier`, `state`, `filter` and `beneath` that says which items it lists, and at least one layout, each with an `id` and a `kind` (`list`, `table`, `board`, `gallery`, `calendar`, `timeline`, `chart` or `map`): + +```bash tab="CLI" tab-group="client" +marfa views create --title "Open tasks" \ + --query '{"types":["core.task"],"filter":"properties.status neq \"completed\""}' \ + --layouts '[{"id":"main","kind":"board","group":{"by":"properties.priority"}}]' +``` + +```http tab="HTTP" +POST /views +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Open tasks", + "query": { "types": ["core.task"], "filter": "properties.status neq \"completed\"" }, + "layouts": [ + { "id": "main", "kind": "board", "group": { "by": "properties.priority" } } + ] +} +``` + +```ts tab="TypeScript" +const { data } = await marfa.POST("/views", { + body: { + title: "Open tasks", + query: { types: ["core.task"], filter: 'properties.status neq "completed"' }, + layouts: [ + { id: "main", kind: "board", group: { by: "properties.priority" } }, + ], + }, +}); +``` + +The answer is the view as a `system.view` item at `version` 1. A view never answers the items it lists: an app runs the `query` itself and shows them as the layout says. A layout only arranges, so it takes no filter of its own. + +- **To change a view,** [update it](/api-reference/views/updateView) with the `version` you read and the settings you change. Each one you name is replaced whole, `layouts` included, and a change based on an older version still applies when nobody changed the same setting since. +- **To delete a view,** [delete it](/api-reference/views/deleteView). It's gone at once, with no trash. +- **To read a view,** [get it](/api-reference/items/getItem) by its `id`, or list the type `system.view`, like any other item. + +Writing a view needs write on `system.view`, and reading one needs read; a `content:*` scope reaches neither. Marfa refuses a malformed setting before it writes anything, and names it in `details.errors[0].path`, such as `layouts.0.group.bucket`. The rules are in the [views](https://github.com/withmarfa/marfa/blob/main/conformance/spec/views.md) chapter of the contract. + ## When a query is refused Marfa refuses a query it can't answer exactly, rather than answer a different one: diff --git a/openapi.json b/openapi.json index 87ded3d3..d152438a 100644 --- a/openapi.json +++ b/openapi.json @@ -1078,6 +1078,133 @@ ], "description": "An item a bulk action left unchanged, and why." }, + "ViewField": { + "type": "string", + "pattern": "^(?:title|created_at|updated_at|occurred_at|type|state|tier|tags|properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "A field: an own field (`title`, `created_at`, `updated_at`, `occurred_at`, `type`, `state`, `tier`, `tags`) or `properties.`.", + "example": "properties.status" + }, + "ViewTier": { + "type": "string", + "enum": [ + "library", + "feed", + "all" + ], + "description": "A tier: `library`, `feed`, or `all` for both." + }, + "ViewLayoutKind": { + "type": "string", + "enum": [ + "list", + "table", + "board", + "gallery", + "calendar", + "timeline", + "chart", + "map" + ], + "description": "How a layout shows items. `list`: rows. `table`: rows with columns. `board`: columns by `group`. `gallery`: cards. `calendar`: on dates. `timeline`: as spans. `chart`: as a chart. `map`: on a map." + }, + "ViewSort": { + "type": "object", + "properties": { + "by": { + "allOf": [ + { + "$ref": "#/components/schemas/ViewField" + }, + { + "description": "The field to sort by." + } + ] + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "`asc` sorts smallest first, `desc` largest first." + } + }, + "required": [ + "by", + "direction" + ], + "additionalProperties": false, + "description": "One key a layout sorts by." + }, + "ViewGroup": { + "type": "object", + "properties": { + "by": { + "type": "string", + "description": "The field to group by: a field, or `edge[]` or `backref[]` for the item an edge of that type joins.", + "example": "properties.status" + }, + "bucket": { + "type": "string", + "enum": [ + "day", + "week", + "month", + "year" + ], + "description": "For a date field, the span each group covers. Absent groups by the value." + }, + "order": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "A value of the field the layout groups by." + }, + "maxItems": 1000, + "description": "The values whose groups come first, in this order. Other groups follow." + }, + "hidden": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "A value of the field the layout groups by." + }, + "maxItems": 1000, + "description": "The values whose groups the layout leaves out." + } + }, + "required": [ + "by" + ], + "additionalProperties": false, + "description": "How a layout groups its items." + }, "InstanceDescription": { "type": "object", "properties": { @@ -6628,191 +6755,394 @@ ], "description": "One copy's latest report and its server-measured age." }, - "KeyResponse": { + "ViewQuery": { "type": "object", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the key." - }, - "key": { - "type": "string", - "description": "The plaintext key, which you send as a bearer token. Save it: no other response shows it." - }, - "label": { - "type": "string", - "description": "A name for the key, to tell it apart from your other keys." - }, - "source": { - "type": "string", - "description": "The key's own source, stamped on the rows it writes unless a write names a source it claims. No other key that hasn't been revoked or expired has it as its own, and it can't change." - }, - "sources": { + "types": { "type": "array", "items": { "type": "string" }, - "description": "Sources the key may also write under, besides its own `source`. Several keys may claim one source, so their writes share natural keys. Empty if the key claims none." + "maxItems": 100, + "description": "The types the view lists, each with its subtypes. Empty or absent lists every type the reading key can read, except `system.*`." }, - "permissions": { + "tier": { + "allOf": [ + { + "$ref": "#/components/schemas/ViewTier" + }, + { + "description": "The tier the view lists. Absent means `all`." + } + ] + }, + "state": { "type": "array", "items": { - "$ref": "#/components/schemas/Permission" + "type": "string", + "enum": [ + "active", + "archived" + ] }, - "description": "The permissions the key holds, such as `audit.read`. Empty if it holds none." + "minItems": 1, + "description": "The states the view lists. Absent means both." }, - "oauth_client_id": { + "filter": { "type": "string", - "description": "The app origin client ID, inherited by every descendant key. Absent when no app is in the key's origin." + "description": "A filter expression, as on `GET /items`, that narrows what the view lists." }, - "default_tier": { + "beneath": { + "type": "string", + "description": "An item ID. The view lists that item and everything under it by `parent-of`, at any depth." + } + }, + "additionalProperties": false, + "description": "Which items the view lists. Absent lists every item the reading key can read, except `system.*`." + }, + "ViewLayout": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "The layout's ID, unique in the view, which an app keeps to reopen it." + }, + "kind": { "allOf": [ { - "$ref": "#/components/schemas/Tier" + "$ref": "#/components/schemas/ViewLayoutKind" }, { - "description": "The tier an item this key creates goes to when the write names none." + "description": "How the layout shows items." } ] }, - "type_permissions": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/TypePermissionLevel" - }, - "description": "Item types the key may `read` or `write`, by type ID or a wildcard such as `core.*` or `*`. `none` denies a type a wildcard covers." - }, - "extension_permissions": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PermissionLevel" - }, - "description": "Extension namespaces the key may `read` or `write`, by namespace or `*`." - }, - "edge_permissions": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PermissionLevel" - }, - "description": "Edge types the key may `read` or `write`, by edge type, a namespace wildcard such as `user.*`, or `*`." + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "description": "The layout's name. Absent, an app names it by its kind." }, - "metadata_permissions": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PermissionLevel" + "sort": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewSort" }, - "description": "Registrations the key may make: `types` to register types and `edge_types` to register edge types, at `write`. `*` covers both." + "maxItems": 10, + "description": "The keys the layout sorts by, first key first." }, - "profile_permissions": { + "group": { "allOf": [ { - "$ref": "#/components/schemas/StoredProfilePermissions" + "$ref": "#/components/schemas/ViewGroup" }, { - "description": "Stored profile levels by field. Only name and avatar permit writes; email is read-only, including under a wildcard." + "description": "How the layout groups its items. A `board` requires it." } ] }, - "enforcement_override": { + "shown": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewField" + }, + "maxItems": 200, + "description": "The fields the layout shows, in order." + }, + "image": { + "type": "string", + "pattern": "^(?:properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "For a `gallery`, the property that holds each card's image." + }, + "date": { + "type": "string", + "pattern": "^(?:created_at|updated_at|occurred_at|properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "For a `calendar`, the date field that places each item." + }, + "start": { + "type": "string", + "pattern": "^(?:created_at|updated_at|occurred_at|properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "For a `timeline`, the date field each span starts at. Required for a `timeline`." + }, + "end": { + "type": "string", + "pattern": "^(?:created_at|updated_at|occurred_at|properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "For a `calendar` or `timeline`, the date field each span ends at. Required for a `timeline`." + }, + "x": { "allOf": [ { - "$ref": "#/components/schemas/EnforcementOverride" + "$ref": "#/components/schemas/ViewField" }, { - "description": "The key's own enforcement levers. Absent if the key sets none, so it follows the instance's." + "description": "For a `chart`, the field along its horizontal axis." } ] }, - "created_at": { - "type": "string", - "description": "When the key was created, in UTC." - }, - "expires_at": { - "type": [ - "string", - "null" + "y": { + "anyOf": [ + { + "type": "string", + "enum": [ + "count" + ] + }, + { + "type": "object", + "properties": { + "sum": { + "type": "string", + "pattern": "^(?:properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "The number property whose values are added up." + } + }, + "required": [ + "sum" + ], + "additionalProperties": false + } ], - "description": "When the key stops working, in UTC, or `null` if it doesn't expire." + "description": "What a chart measures: `count` of items, or `{ \"sum\": \"properties.\" }`." }, - "last_used_at": { - "type": [ - "string", - "null" - ], - "description": "When the key's use was last recorded, in UTC, or `null` if none is recorded. Marfa records usage asynchronously at most once an hour; a busy or unavailable store can delay or drop a record." + "location": { + "type": "string", + "pattern": "^(?:properties\\.[A-Za-z_][A-Za-z0-9_]*)$", + "description": "For a `map`, the property that holds each item's place." } }, "required": [ "id", - "key", - "label", - "source", - "sources", - "permissions", - "default_tier", - "type_permissions", - "extension_permissions", - "edge_permissions", - "metadata_permissions", - "profile_permissions", - "created_at", - "expires_at", - "last_used_at" + "kind" ], - "description": "A new API key, with its plaintext `key`." - }, - "Permission": { - "type": "string", - "enum": [ - "webhooks.manage", - "schema.write", - "config.manage", - "audit.read", - "items.purge", - "keys.mint", - "grants.manage", - "instance.read", - "instance.maintain", - "connectors.manage", - "blobs.manage", - "keys.manage" - ], - "description": "A permission a credential can hold, for an operation the type, edge, extension, metadata and profile maps don't cover. `schema.write` replaces and deletes types and edge types; `keys.mint` mints keys and lists, changes and revokes those within the caller's reach; `keys.manage` lists and revokes every key and changes one without widening it; `items.purge` purges trashed items; `webhooks.manage` manages webhooks; `config.manage` reads and replaces `/config`; `audit.read` reads the audit log; `grants.manage` lists and revokes other apps' access; `instance.read` reads health, metrics, housekeeping, retired platform types, blob storage reports and every bulk job's status; `instance.maintain` runs housekeeping, resets platform type definitions and cancels bulk jobs; `connectors.manage` administers connector registrations and their endpoints, and clears their retained state; and `blobs.manage` manages every blob." - }, - "TypePermissionLevel": { - "type": "string", - "enum": [ - "read", - "write", - "none" - ], - "description": "What a key may do with a type: `read` it, `write` it (which includes reading), or `none`, which denies a type a wildcard entry would reach." + "additionalProperties": false, + "description": "One way a view shows its items. A layout takes only the options its `kind` takes, and never narrows which items the view lists." }, - "ApiKeyPage": { + "InvalidIdOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKey" + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "invalid_id", + "invalid_transition", + "missing_required_field", + "unknown_type", + "validation_error" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } }, - "description": "Every key you can reach." - }, - "next_cursor": { - "type": [ - "string", - "null" + "required": [ + "code", + "message" ], - "description": "Always `null`: Marfa returns every key you can reach in one page." + "description": "What went wrong." } }, "required": [ - "data", - "next_cursor" + "error" ], - "description": "A page holding every key you can reach." + "description": "An error response." }, - "ApiKey": { + "KeyResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the key." + }, + "key": { + "type": "string", + "description": "The plaintext key, which you send as a bearer token. Save it: no other response shows it." + }, + "label": { + "type": "string", + "description": "A name for the key, to tell it apart from your other keys." + }, + "source": { + "type": "string", + "description": "The key's own source, stamped on the rows it writes unless a write names a source it claims. No other key that hasn't been revoked or expired has it as its own, and it can't change." + }, + "sources": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sources the key may also write under, besides its own `source`. Several keys may claim one source, so their writes share natural keys. Empty if the key claims none." + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "description": "The permissions the key holds, such as `audit.read`. Empty if it holds none." + }, + "oauth_client_id": { + "type": "string", + "description": "The app origin client ID, inherited by every descendant key. Absent when no app is in the key's origin." + }, + "default_tier": { + "allOf": [ + { + "$ref": "#/components/schemas/Tier" + }, + { + "description": "The tier an item this key creates goes to when the write names none." + } + ] + }, + "type_permissions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TypePermissionLevel" + }, + "description": "Item types the key may `read` or `write`, by type ID or a wildcard such as `core.*` or `*`. `none` denies a type a wildcard covers." + }, + "extension_permissions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PermissionLevel" + }, + "description": "Extension namespaces the key may `read` or `write`, by namespace or `*`." + }, + "edge_permissions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PermissionLevel" + }, + "description": "Edge types the key may `read` or `write`, by edge type, a namespace wildcard such as `user.*`, or `*`." + }, + "metadata_permissions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PermissionLevel" + }, + "description": "Registrations the key may make: `types` to register types and `edge_types` to register edge types, at `write`. `*` covers both." + }, + "profile_permissions": { + "allOf": [ + { + "$ref": "#/components/schemas/StoredProfilePermissions" + }, + { + "description": "Stored profile levels by field. Only name and avatar permit writes; email is read-only, including under a wildcard." + } + ] + }, + "enforcement_override": { + "allOf": [ + { + "$ref": "#/components/schemas/EnforcementOverride" + }, + { + "description": "The key's own enforcement levers. Absent if the key sets none, so it follows the instance's." + } + ] + }, + "created_at": { + "type": "string", + "description": "When the key was created, in UTC." + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "description": "When the key stops working, in UTC, or `null` if it doesn't expire." + }, + "last_used_at": { + "type": [ + "string", + "null" + ], + "description": "When the key's use was last recorded, in UTC, or `null` if none is recorded. Marfa records usage asynchronously at most once an hour; a busy or unavailable store can delay or drop a record." + } + }, + "required": [ + "id", + "key", + "label", + "source", + "sources", + "permissions", + "default_tier", + "type_permissions", + "extension_permissions", + "edge_permissions", + "metadata_permissions", + "profile_permissions", + "created_at", + "expires_at", + "last_used_at" + ], + "description": "A new API key, with its plaintext `key`." + }, + "Permission": { + "type": "string", + "enum": [ + "webhooks.manage", + "schema.write", + "config.manage", + "audit.read", + "items.purge", + "keys.mint", + "grants.manage", + "instance.read", + "instance.maintain", + "connectors.manage", + "blobs.manage", + "keys.manage" + ], + "description": "A permission a credential can hold, for an operation the type, edge, extension, metadata and profile maps don't cover. `schema.write` replaces and deletes types and edge types; `keys.mint` mints keys and lists, changes and revokes those within the caller's reach; `keys.manage` lists and revokes every key and changes one without widening it; `items.purge` purges trashed items; `webhooks.manage` manages webhooks; `config.manage` reads and replaces `/config`; `audit.read` reads the audit log; `grants.manage` lists and revokes other apps' access; `instance.read` reads health, metrics, housekeeping, retired platform types, blob storage reports and every bulk job's status; `instance.maintain` runs housekeeping, resets platform type definitions and cancels bulk jobs; `connectors.manage` administers connector registrations and their endpoints, and clears their retained state; and `blobs.manage` manages every blob." + }, + "TypePermissionLevel": { + "type": "string", + "enum": [ + "read", + "write", + "none" + ], + "description": "What a key may do with a type: `read` it, `write` it (which includes reading), or `none`, which denies a type a wildcard entry would reach." + }, + "ApiKeyPage": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + }, + "description": "Every key you can reach." + }, + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "Always `null`: Marfa returns every key you can reach in one page." + } + }, + "required": [ + "data", + "next_cursor" + ], + "description": "A page holding every key you can reach." + }, + "ApiKey": { "type": "object", "properties": { "id": { @@ -35388,18 +35718,1135 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + } + } + } + }, + "/folders/{id}/revoke": { + "post": { + "operationId": "revokeFolder", + "tags": [ + "Folders" + ], + "summary": "Revoke a folder", + "description": "Revokes a folder: moves it to `revoked`, a terminal state, and sets `revoked_at`. The items placed in it keep their `in-folder` edges.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the folder." + }, + "required": true, + "description": "The ID of the folder.", + "name": "id", + "in": "path" + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ], + "responses": { + "200": { + "description": "Returns the revoked folder and its metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemWithMetadata" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "400": { + "description": "- `invalid_id`: the ID is malformed.\n- `invalid_transition`: the folder is already revoked.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: you don't have write on `system.folder`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeNotPermittedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "404": { + "description": "- `item_not_found`: no `system.folder` has this ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + }, + { + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" + } + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + } + } + } + }, + "/folders/{id}/status": { + "put": { + "operationId": "reportFolderStatus", + "tags": [ + "Folders" + ], + "summary": "Report a folder status", + "description": "Reports your sign-in's status for one folder copy, replacing its earlier report, and returns the latest accepted sequence and receipt time. An equal or older sequence leaves the stored report unchanged.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the folder." + }, + "required": true, + "description": "The ID of the folder.", + "name": "id", + "in": "path" + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FolderStatusReport" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the latest accepted sequence and receipt time for your copy.", + "content": { + "application/json": { + "example": { + "accepted": true, + "sequence": 1, + "received_at": "2026-10-03T09:30:00.000Z" + }, + "schema": { + "$ref": "#/components/schemas/FolderStatusAck" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `invalid_id`: the folder ID is malformed.\n- `validation_error`: a field, count, time, code, or undeclared field is invalid.\n- `missing_required_field`: a required report field is missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "403": { + "description": "- `type_not_permitted`: your credential reads no type.\n- `forbidden`: a local command has no sign-in to report under.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "404": { + "description": "- `item_not_found`: the folder does not exist or you cannot read it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + } + }, + "get": { + "operationId": "getFolderStatus", + "tags": [ + "Folders" + ], + "summary": "Get a folder status", + "description": "Returns the latest folder-copy reports grouped by sign-in, with receipt ages and totals from sign-ins that still stand. Historical reports remain visible.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the folder." + }, + "required": true, + "description": "The ID of the folder.", + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Returns current and historical reports and the current totals.", + "content": { + "application/json": { + "example": { + "folder_id": "01920000-0000-7000-8000-000000000002", + "sign_ins": [ + { + "sign_in": { + "kind": "key", + "id": "01920000-0000-7000-8000-000000000001", + "name": "Folder sync" + }, + "current": true, + "copies": [ + { + "folder_store_id": "01920000-0000-7000-8000-000000000000", + "sequence": 1, + "last_success_at": null, + "pending_uploads": 2, + "pending_downloads": 0, + "paused_removals": 0, + "first_sync_waiting": false, + "problems": [], + "received_at": "2026-10-03T09:30:00.000Z", + "age_seconds": 60 + } + ] + } + ], + "totals": { + "saturated": false, + "pending_uploads": 2, + "pending_downloads": 0, + "paused_removals": 0, + "first_sync_waiting": 0, + "problems": [] + } + }, + "schema": { + "$ref": "#/components/schemas/FolderStatus" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `invalid_id`: the folder ID is malformed.\n- `validation_error`: the query names an undeclared field, or the request asks for an unsupported conditional copy read.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidIdOrValidationErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -35410,17 +36857,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "422": { - "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "403": { + "description": "- `type_not_permitted`: your credential reads no type.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + "$ref": "#/components/schemas/TypeNotPermittedRefusal" } } }, @@ -35442,9 +36898,36 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "404": { + "description": "- `item_not_found`: the folder does not exist or you cannot read it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemNotFoundRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, @@ -35478,9 +36961,6 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -35511,9 +36991,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -35544,9 +37021,6 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -35577,63 +37051,63 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" } } } } } }, - "/folders/{id}/revoke": { + "/views": { "post": { - "operationId": "revokeFolder", + "operationId": "createView", "tags": [ - "Folders" + "Views" ], - "summary": "Revoke a folder", - "description": "Revokes a folder: moves it to `revoked`, a terminal state, and sets `revoked_at`. The items placed in it keep their `in-folder` edges.", + "summary": "Create a view", + "description": "Creates a view: a `system.view` item whose properties say which items it lists and how its layouts show them. The item endpoints can't write one.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the folder." - }, - "required": true, - "description": "The ID of the folder.", - "name": "id", - "in": "path" - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "type": "string", - "maxLength": 255 - }, - "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." - }, - { - "name": "X-Marfa-Instance-Id", - "in": "header", - "required": false, - "schema": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "description": "The view's title." + }, + "query": { + "$ref": "#/components/schemas/ViewQuery" + }, + "layouts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewLayout" + }, + "minItems": 1, + "maxItems": 50, + "description": "How the view shows its items, in order. At least one." + } + }, + "required": [ + "title", + "layouts" + ], + "additionalProperties": false + } + } } - ], + }, "responses": { - "200": { - "description": "Returns the revoked folder and its metadata.", + "201": { + "description": "Returns the new view, a `system.view` item, and its metadata.", "content": { "application/json": { "schema": { @@ -35663,11 +37137,11 @@ } }, "400": { - "description": "- `invalid_id`: the ID is malformed.\n- `invalid_transition`: the folder is already revoked.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", + "description": "- `validation_error`: a setting is malformed or undeclared, such as a repeated layout `id`. `details.errors[0].path` or `details.unknown_body_fields` names it.\n- `missing_required_field`: `title`, `layouts` or a part a layout requires is missing. `details.field` names it.\n- `unknown_type`: `query.types` names a type that isn't registered.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrValidationErrorRefusal" + "$ref": "#/components/schemas/MissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" } } }, @@ -35726,7 +37200,7 @@ } }, "403": { - "description": "- `type_not_permitted`: you don't have write on `system.folder`.", + "description": "- `type_not_permitted`: you don't have write on `system.view`.", "content": { "application/json": { "schema": { @@ -35755,39 +37229,6 @@ } } }, - "404": { - "description": "- `item_not_found`: no `system.folder` has this ID.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemNotFoundRefusal" - } - } - }, - "headers": { - "X-Marfa-Contract": { - "$ref": "#/components/headers/X-Marfa-Contract" - }, - "X-Request-ID": { - "$ref": "#/components/headers/X-Request-ID" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - }, - "X-Error-Code": { - "$ref": "#/components/headers/X-Error-Code" - }, - "Idempotency-Replayed": { - "$ref": "#/components/headers/Idempotency-Replayed" - } - } - }, "409": { "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", "content": { @@ -36017,17 +37458,39 @@ } } } - } + }, + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." + } + ] } }, - "/folders/{id}/status": { - "put": { - "operationId": "reportFolderStatus", + "/views/{id}": { + "patch": { + "operationId": "updateView", "tags": [ - "Folders" + "Views" ], - "summary": "Report a folder status", - "description": "Reports your sign-in's status for one folder copy, replacing its earlier report, and returns the latest accepted sequence and receipt time. An equal or older sequence leaves the stored report unchanged.", + "summary": "Update a view", + "description": "Updates a view's title, query or layouts. Each one you send replaces the current value whole, and those you leave out stay as they are. Send the `version` you read.", "security": [ { "bearerAuth": [] @@ -36037,13 +37500,23 @@ { "schema": { "type": "string", - "description": "The ID of the folder." + "description": "The ID of the view." }, "required": true, - "description": "The ID of the folder.", + "description": "The ID of the view.", "name": "id", "in": "path" }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, { "name": "X-Marfa-Instance-Id", "in": "header", @@ -36060,23 +37533,54 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FolderStatusReport" + "type": "object", + "properties": { + "version": { + "type": "integer", + "minimum": 0, + "description": "The version of the view you read. If the view has changed since, Marfa merges your change where nothing collides." + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "description": "A new title for the view." + }, + "query": { + "allOf": [ + { + "$ref": "#/components/schemas/ViewQuery" + }, + { + "description": "A new query, which replaces the current one whole." + } + ] + }, + "layouts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ViewLayout" + }, + "minItems": 1, + "maxItems": 50, + "description": "New layouts, which replace the current ones whole. At least one." + } + }, + "required": [ + "version" + ], + "additionalProperties": false } } } }, "responses": { "200": { - "description": "Returns the latest accepted sequence and receipt time for your copy.", + "description": "Returns the updated view and its metadata.", "content": { "application/json": { - "example": { - "accepted": true, - "sequence": 1, - "received_at": "2026-10-03T09:30:00.000Z" - }, "schema": { - "$ref": "#/components/schemas/FolderStatusAck" + "$ref": "#/components/schemas/ItemWithMetadata" } } }, @@ -36095,15 +37599,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, "400": { - "description": "- `invalid_id`: the folder ID is malformed.\n- `validation_error`: a field, count, time, code, or undeclared field is invalid.\n- `missing_required_field`: a required report field is missing.", + "description": "- `validation_error`: no setting, or a malformed or undeclared setting; `details` names it.\n- `missing_required_field`: `version` or a required layout part is missing; `details.field` names it.\n- `unknown_type`: `query.types` names a type that isn't registered.\n- `invalid_id`: the ID is invalid.\n- `invalid_transition`: the view is revoked.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" } } }, @@ -36125,6 +37632,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36159,11 +37669,11 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reads no type.\n- `forbidden`: a local command has no sign-in to report under.", + "description": "- `type_not_permitted`: you don't have write on `system.view`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal" + "$ref": "#/components/schemas/TypeNotPermittedRefusal" } } }, @@ -36189,7 +37699,7 @@ } }, "404": { - "description": "- `item_not_found`: the folder does not exist or you cannot read it.", + "description": "- `item_not_found`: no `system.view` has this ID.", "content": { "application/json": { "schema": { @@ -36215,15 +37725,31 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, "409": { - "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "description": "- `version_conflict`: a setting you sent has changed since `version`. `conflicting_fields` names it.\n- `ancestor_unavailable`: Marfa holds no snapshot of `version` to merge from.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InstanceChangedRefusal" + "anyOf": [ + { + "$ref": "#/components/schemas/ItemVersionConflict" + }, + { + "$ref": "#/components/schemas/ItemAncestorUnavailable" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + }, + { + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" + } + ] } } }, @@ -36245,6 +37771,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36269,6 +37798,39 @@ } } }, + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, "429": { "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", "content": { @@ -36299,6 +37861,9 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36329,6 +37894,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36359,6 +37927,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36389,18 +37960,21 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } } } }, - "get": { - "operationId": "getFolderStatus", + "delete": { + "operationId": "deleteView", "tags": [ - "Folders" + "Views" ], - "summary": "Get a folder status", - "description": "Returns the latest folder-copy reports grouped by sign-in, with receipt ages and totals from sign-ins that still stand. Historical reports remain visible.", + "summary": "Delete a view", + "description": "Deletes an active or revoked view permanently, with no trash. Marfa announces it as `item.purged`, so every app drops it. The items it listed don't change.", "security": [ { "bearerAuth": [] @@ -36410,56 +37984,41 @@ { "schema": { "type": "string", - "description": "The ID of the folder." + "description": "The ID of the view." }, "required": true, - "description": "The ID of the folder.", + "description": "The ID of the view.", "name": "id", "in": "path" + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A unique key that makes the request safe to retry. If you send the same request with the same key again, Marfa returns the first response and doesn't write again. A key belongs to the credential that sends it." + }, + { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "description": "The ID of the instance you expect, as `GET /` returns it. If another instance answers at this address, Marfa returns `409 instance_changed` and doesn't write. Leave it out to send the request to whichever instance answers." } ], "responses": { "200": { - "description": "Returns current and historical reports and the current totals.", + "description": "Returns `ok: true`.", "content": { "application/json": { - "example": { - "folder_id": "01920000-0000-7000-8000-000000000002", - "sign_ins": [ - { - "sign_in": { - "kind": "key", - "id": "01920000-0000-7000-8000-000000000001", - "name": "Folder sync" - }, - "current": true, - "copies": [ - { - "folder_store_id": "01920000-0000-7000-8000-000000000000", - "sequence": 1, - "last_success_at": null, - "pending_uploads": 2, - "pending_downloads": 0, - "paused_removals": 0, - "first_sync_waiting": false, - "problems": [], - "received_at": "2026-10-03T09:30:00.000Z", - "age_seconds": 60 - } - ] - } - ], - "totals": { - "saturated": false, - "pending_uploads": 2, - "pending_downloads": 0, - "paused_removals": 0, - "first_sync_waiting": 0, - "problems": [] - } - }, "schema": { - "$ref": "#/components/schemas/FolderStatus" + "$ref": "#/components/schemas/Ok" } } }, @@ -36478,11 +38037,14 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, "400": { - "description": "- `invalid_id`: the folder ID is malformed.\n- `validation_error`: the query names an undeclared field, or the request asks for an unsupported conditional copy read.", + "description": "- `invalid_id`: the ID is malformed.\n- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { @@ -36508,6 +38070,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36542,7 +38107,7 @@ } }, "403": { - "description": "- `type_not_permitted`: your credential reads no type.", + "description": "- `type_not_permitted`: you don't have write on `system.view`.", "content": { "application/json": { "schema": { @@ -36572,7 +38137,7 @@ } }, "404": { - "description": "- `item_not_found`: the folder does not exist or you cannot read it.", + "description": "- `item_not_found`: no `system.view` has this ID.", "content": { "application/json": { "schema": { @@ -36598,6 +38163,103 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + }, + { + "$ref": "#/components/schemas/IdempotencyKeyInFlightRefusal" + } + ] + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "422": { + "description": "- `idempotency_key_reused`: the key was first used for a different request. Nothing is written.\n- `idempotency_result_not_retained`: the first response was too large to keep, so Marfa can't replay it. The write isn't repeated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdempotencyKeyReusedOrIdempotencyResultNotRetainedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36631,6 +38293,9 @@ }, "Retry-After": { "$ref": "#/components/headers/Retry-After" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36661,6 +38326,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36691,6 +38359,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } }, @@ -36721,6 +38392,9 @@ }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" + }, + "Idempotency-Replayed": { + "$ref": "#/components/headers/Idempotency-Replayed" } } } @@ -56305,6 +57979,15 @@ "getFolderStatus" ] }, + { + "name": "Views", + "description": "Saved lists of items and how to show them, held as `system.view` items.", + "x-operation-order": [ + "createView", + "updateView", + "deleteView" + ] + }, { "name": "Connectors", "description": "Processes outside the server that read and write on your behalf, with their heartbeats and runs.", diff --git a/src/generated/GLOSSARY.md b/src/generated/GLOSSARY.md index 2e8e1a7f..e8beaf61 100644 --- a/src/generated/GLOSSARY.md +++ b/src/generated/GLOSSARY.md @@ -20,6 +20,7 @@ The words are grouped by topic. | **tier** | Which layer an item sits in. `feed` holds what arrives in volume and as it came, from connectors and capture, under the source's own type, unsorted until a person or an app moves it to the library or archives it. `library` holds what a person chose to keep, modeled in a core type or a child of one. An item **moves to the library** or **moves to the feed** by a change of its `tier` (`PATCH /items/{id}`): it is the same item, not a copy, and a connector's type must be readable as a core type, by `parent` or `compatible_with`, for the moved item to be read as one. Not a permission and not a lifecycle. The **namespace tier** (`core`, `system`, `app`, `user`, `marfa`, publisher) is a different axis and is not this word; say "namespace" for it. | | **link** | The value a row holds in the property its type names as `link_field`: the row's own id at the vendor that writes the type, by which a connector finds the row. One row's within the type, in every state, and kept as a tombstone when the row is purged. A type's own, never a parent's. The word keeps its ordinary sense of a URL, a blob's link or a `[[name]]` in a Markdown body, where the sentence says which. | | **tombstone** | What a purge leaves of a row under its type: its link and its natural key `(source, source_id)`, each with `purged_at` and a `settled_at`, the purge time or the later time of the vendor's own change a connector made in carrying the purge out, moved only later. An entry from the vendor naming the key is a new row only if the vendor changed it after `settled_at`. Kept, never swept, until a row holds the key again or the type goes: one small row per purged key. A link change drops the old link's; registering a type clears its identifier's. A trashed row leaves none. Not a trashed row, nor a revoked grant row. | +| **view** | A saved list of items, held as a `system.view` and shared by every app: its query says which items it lists, and its layouts say how each one shows them, as a list, a table, a board, a gallery, a calendar, a timeline, a chart or a map. Written only through `/views`. Not a folder, which holds items as files on a machine. | ### Properties, metadata and edges @@ -152,12 +153,12 @@ Access is read the same way wherever a person sees it: on the pages where they a ### The CLI -| Word | What it means | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **CLI** | The `marfa` program: the reference client of an instance, and the subject of a rule for what only it does. Everything it does is a **command**. | -| **command** | One command of the CLI, such as `marfa items list`. Every published operation is a command under its area (`status`, `metrics`, `items`, `edges`, `edge-types`, `types`, `search`, `metadata`, `extensions`, `blobs`, `keys`, `config`, `export`, `restore`, `webhooks`, `audit`, `events`, `housekeeping`, `connectors`, `owner`, `setup`, `login`), the working copy sits under `device`, and `folders` is a root of its own. `whoami` reads the root as `status` does, and names the credential. Private setup and recovery commands use `--socket`. Three other roots reach no published operation and are commands all the same: `logout` for the keychain, `operations` for the table itself and `docs` for the public docs site. MCP joins them last. | -| **binary** | The built file of the CLI. In prose about what it does, say **CLI**. | -| **profile** | Two things, so a sentence names which. **The owner's profile** is the owner's name, email address and avatar on the instance, which `marfa profile update` changes and the `profile` access row and `--profile-permission` grant. **A profile in `profiles.toml`** is a section of the CLI's own file, written by hand, that names a server and where its key comes from, and that `--profile` or `MARFA_PROFILE` picks. | +| Word | What it means | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **CLI** | The `marfa` program: the reference client of an instance, and the subject of a rule for what only it does. Everything it does is a **command**. | +| **command** | One command of the CLI, such as `marfa items list`. Every published operation is a command under its area (`status`, `metrics`, `items`, `edges`, `edge-types`, `types`, `search`, `metadata`, `extensions`, `blobs`, `keys`, `config`, `export`, `restore`, `views`, `webhooks`, `audit`, `events`, `housekeeping`, `connectors`, `owner`, `setup`, `login`), the working copy sits under `device`, and `folders` is a root of its own. `whoami` reads the root as `status` does, and names the credential. Private setup and recovery commands use `--socket`. Three other roots reach no published operation and are commands all the same: `logout` for the keychain, `operations` for the table itself and `docs` for the public docs site. MCP joins them last. | +| **binary** | The built file of the CLI. In prose about what it does, say **CLI**. | +| **profile** | Two things, so a sentence names which. **The owner's profile** is the owner's name, email address and avatar on the instance, which `marfa profile update` changes and the `profile` access row and `--profile-permission` grant. **A profile in `profiles.toml`** is a section of the CLI's own file, written by hand, that names a server and where its key comes from, and that `--profile` or `MARFA_PROFILE` picks. | ## Banned @@ -216,8 +217,8 @@ Its sibling `updated_before` closes the same window at the top and is **exclusiv ## Errors - `unknown_type`: there is no such type. A lookup answer. -- `type_not_permitted`: the type is not one this credential may reach here. A `403`, decided by the namespace and the credential's permission map rather than by the identifier. It consults no registry, so it is not a statement that the type exists. On an operation that looks up the item an id names, it means the credential may read the item's type and not write it, or reaches no type at all, and it is never the answer for an item whose type the credential may not read. The folder settings operations are the exception: they refuse a key without write on `system.folder` before looking any row up. Folder status reads and reports use folder read access. On the blob operations it refuses a credential reaching no type before the hash is read, and an upload from one that writes no type; a blob it may not read answers `blob_not_found`, as an unknown hash does. -- `item_not_found`: no item this credential may read has this id, or, on the folder operations, no folder: a readable item that is not a `system.folder` answers it there too. An item of a type it may not read answers exactly as one that does not exist, so the answer is not a statement that no such row exists. A key learns its own reach from `GET /keys/current`, and a signed-in app's token from the scope list on its token response, never from a refusal. +- `type_not_permitted`: the type is not one this credential may reach here. A `403`, decided by the namespace and the credential's permission map rather than by the identifier. It consults no registry, so it is not a statement that the type exists. On an operation that looks up the item an id names, it means the credential may read the item's type and not write it, or reaches no type at all, and it is never the answer for an item whose type the credential may not read. The folder settings and view operations are the exception: they refuse a key without write on `system.folder` or `system.view` before looking any row up. Folder status reads and reports use folder read access. On the blob operations it refuses a credential reaching no type before the hash is read, and an upload from one that writes no type; a blob it may not read answers `blob_not_found`, as an unknown hash does. +- `item_not_found`: no item this credential may read has this id, or, on the folder operations, no folder: a readable item that is not a `system.folder` answers it there too, as one that is not a `system.view` does on the view operations. An item of a type it may not read answers exactly as one that does not exist, so the answer is not a statement that no such row exists. A key learns its own reach from `GET /keys/current`, and a signed-in app's token from the scope list on its token response, never from a refusal. - `api_key_not_found`: no key within this credential's reach has this id. A key it could not have minted answers exactly as one that does not exist, as `item_not_found` does for an item it may not read. - A **malformed** identifier is neither: it never reached a lookup, so it answers the generic `validation_error` with the field named. - `instance_changed`: a request named, in `X-Marfa-Instance-Id`, an instance other than the one at the address, so it reached a different instance from the one its sender's copy came from. A `409`, judged before anything about the request but its credential, and never recorded under an `Idempotency-Key`. Not `reach_changed`, which says the reach a credential holds on the same instance has changed. diff --git a/src/generated/catalog/builtin-catalog.json b/src/generated/catalog/builtin-catalog.json index 8803eb15..a985e9e3 100644 --- a/src/generated/catalog/builtin-catalog.json +++ b/src/generated/catalog/builtin-catalog.json @@ -1679,6 +1679,29 @@ "description": "When the folder was revoked" } } + }, + { + "id": "system.view", + "label": "View", + "description": "A saved list of items, held here and shared by every app: which items, and how each layout shows them. Written only through `/views`: created, changed at a version, and deleted, which removes it at once with no trash. Lifecycle bounded to active | revoked. Has no tier.", + "version": 0, + "fields": { + "title": { + "type": "string", + "description": "The view's name", + "required": true + }, + "query": { + "type": "object", + "description": "Which items the view lists. `types`: type identifiers, each with its subtypes; empty or absent lists every type the reading key reads. `tier`: `library`, `feed` or `all`; absent is `all`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`. `beneath`: an item id; the item and everything under it by `parent-of`." + }, + "layouts": { + "type": "array", + "description": "How the view shows its items, in order, at least one. Each layout has an `id` unique in the view, a `kind` (`list`, `table`, `board`, `gallery`, `calendar`, `timeline`, `chart` or `map`), and an optional `title`, `sort`, `group`, `shown` and the options its kind takes. A layout never narrows which items the view lists", + "required": true, + "items_type": "object" + } + } } ], "edge_types": [ diff --git a/src/generated/catalog/system.view.md b/src/generated/catalog/system.view.md new file mode 100644 index 00000000..37af68d7 --- /dev/null +++ b/src/generated/catalog/system.view.md @@ -0,0 +1,7 @@ +## Fields + +| Field | Kind | Required | Description | +| --------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | `string` | Yes | The view's name | +| `query` | `object` | No | Which items the view lists. `types`: type identifiers, each with its subtypes; empty or absent lists every type the reading key reads. `tier`: `library`, `feed` or `all`; absent is `all`. `state`: `active`, `archived` or both; absent is both. `filter`: an expression in the listing grammar's `filter`. `beneath`: an item id; the item and everything under it by `parent-of`. | +| `layouts` | `array` of `object` | Yes | How the view shows its items, in order, at least one. Each layout has an `id` unique in the view, a `kind` (`list`, `table`, `board`, `gallery`, `calendar`, `timeline`, `chart` or `map`), and an optional `title`, `sort`, `group`, `shown` and the options its kind takes. A layout never narrows which items the view lists | diff --git a/src/generated/catalog/types.md b/src/generated/catalog/types.md index 560bcb50..efbdb2b2 100644 --- a/src/generated/catalog/types.md +++ b/src/generated/catalog/types.md @@ -14,7 +14,8 @@ ## System types -| Type | Description | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`system.connection`](/catalog/connection) | An OAuth client this user has authorized, written when the user approves the client's consent or device sign-in. `kind` is `app`. Lifecycle bounded to active \| revoked. Has no tier. | -| [`system.folder`](/catalog/folder) | A folder's settings, held here and shared by every machine bound to the folder; each machine chooses only the directory. The settings are written only through `/folders`: created, changed at a version, and revoked. Lifecycle bounded to active \| revoked. Has no tier. | +| Type | Description | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`system.connection`](/catalog/connection) | An OAuth client this user has authorized, written when the user approves the client's consent or device sign-in. `kind` is `app`. Lifecycle bounded to active \| revoked. Has no tier. | +| [`system.folder`](/catalog/folder) | A folder's settings, held here and shared by every machine bound to the folder; each machine chooses only the directory. The settings are written only through `/folders`: created, changed at a version, and revoked. Lifecycle bounded to active \| revoked. Has no tier. | +| [`system.view`](/catalog/view) | A saved list of items, held here and shared by every app: which items, and how each layout shows them. Written only through `/views`: created, changed at a version, and deleted, which removes it at once with no trash. Lifecycle bounded to active \| revoked. Has no tier. | diff --git a/src/generated/cli-commands.json b/src/generated/cli-commands.json index 5ab4d48c..e82bd0f3 100644 --- a/src/generated/cli-commands.json +++ b/src/generated/cli-commands.json @@ -123,6 +123,11 @@ "config update": {"options": [{"flag": "--file", "value": true}, {"flag": "--body", "value": true}]}, "export": {"options": [{"flag": "--format", "value": true}, {"flag": "--output", "value": true}, {"flag": "--type", "value": true}, {"flag": "--state", "value": true}, {"flag": "--source", "value": true}, {"flag": "--occurred-after", "value": true}, {"flag": "--occurred-before", "value": true}]}, "restore": {"options": [], "positional": true}, + "views": {"options": [], "subcommands": true}, + "views create": {"options": [{"flag": "--file", "value": true}, {"flag": "--body", "value": true}, {"flag": "--title", "value": true}, {"flag": "--query", "value": true}, {"flag": "--layouts", "value": true}, {"flag": "--idempotency-key", "value": true}]}, + "views change": {"options": [{"flag": "--version", "value": true}, {"flag": "--file", "value": true}, {"flag": "--body", "value": true}, {"flag": "--title", "value": true}, {"flag": "--query", "value": true}, {"flag": "--layouts", "value": true}, {"flag": "--idempotency-key", "value": true}], "positional": true}, + "views delete": {"options": [{"flag": "--idempotency-key", "value": true}], "positional": true}, + "views get": {"options": [], "positional": true}, "webhooks": {"options": [], "subcommands": true}, "webhooks create": {"options": [{"flag": "--to", "value": true}, {"flag": "--event", "value": true}, {"flag": "--type-filter", "value": true}, {"flag": "--secret", "value": true}], "positional": true}, "webhooks list": {"options": []}, diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index 86e49ffe..e48c58fc 100644 --- a/src/generated/cli-download.md +++ b/src/generated/cli-download.md @@ -1,5 +1,5 @@ ```bash -curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.28/marfa-0.0.28-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.29/marfa-0.0.29-darwin-arm64.tar.gz | tar -xz sudo mkdir -p /usr/local/bin sudo mv marfa /usr/local/bin/ marfa --version diff --git a/src/generated/contract-index.json b/src/generated/contract-index.json index 589205e6..d1ef7f3a 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -1409,7 +1409,7 @@ "errors/storage-full-unknown": "a41bc3e6fe01cc5c", "errors/storage-full-declared": "678e209c5c9b2aa6", "errors/storage-reserve-details": "43a6ceaad0b45bc5", - "errors/grant-type": "09100b07faf5a7ed", + "errors/grant-type": "4c69f480eee0a4b0", "errors/grant-edge-type": "7d27acb5131562bd", "errors/grant-extension": "d779df5cd9e0effa", "errors/grant-level": "232145a10953d0e5", @@ -2260,12 +2260,12 @@ "folders/write-read-only": "f3be6bf64000cdd9", "folders/delete-grace": "2ccbfafe7259bb46", "folders/delete-rename-cancels": "2f43bd8c739b162b", - "folders/delete-remote-revives": "755cee498f676bc1", + "folders/delete-remote-revives": "160320d939841ac2", "folders/delete-revived-cancels": "e6541a9f3c3b1f7e", "folders/delete-revived-count": "444d2573dc949c88", "folders/delete-unshown-change": "4447a7ff8d48eaa3", "folders/delete-before-first-write": "c0d8f9f19c8dbd90", - "folders/pull-missing-file-not-written": "2515b8c825ff8d6d", + "folders/pull-missing-file-not-written": "200f976897437240", "folders/pull-missing-file-unwritten-count": "cf6c8380bf09dbc2", "folders/pull-missing-file-moved-keeps-item": "59828aec3d896a39", "folders/delete-path-reused": "1662a3ba2cc77550", @@ -2406,20 +2406,22 @@ "folders/moved-carried-id-utf16": "aa25b2589c7fe12a", "folders/peer-owned-bytes-not-move": "b5a37ef19c93a44d", "folders/both-folders-copy": "18aa2ad3dde9f685", - "folders/missing-file-trashed": "235209e5aa435b25", - "folders/missing-file-trashed-path-reported": "b45a664a6c75e938", + "folders/missing-file-trashed": "06e737a76cd122cb", + "folders/missing-file-trashed-path-reported": "2aabd6f32edecfb6", + "folders/missing-file-changed-revives": "3bf22b95f4f82ec7", "folders/missing-file-said": "b3c5c5178e5a1e3f", "folders/uncertain-move-deferred": "26ddcaa6fb20dbd5", "folders/uncertain-move-deferred-unsure-reported": "a44b92875008474a", - "folders/uncertain-move-no-replacement": "24ce7c30e8f7bf42", + "folders/uncertain-move-no-replacement": "00392605487ee596", "folders/uncertain-move-not-text": "986c81a1f5d0d8b1", "folders/moved-arrival-bytes": "cf169fa2cdd3a94f", "folders/moved-arrival-text-note": "57c5d55561fb9ca2", "folders/moved-arrival-markdown-new": "a36da7914451782d", "folders/moved-file-item-bytes": "aa7b1188ad718f00", "folders/moved-binding-kept": "ab532e8f9a1b8e84", - "folders/moved-awaiting-peer": "2803d61c8f98a691", - "folders/moved-awaiting-peer-no-replacement": "ac61bc6fceeeaaa7", + "folders/moved-awaiting-peer": "f0e28b581c370b88", + "folders/moved-awaiting-peer-no-replacement": "18fb2ba20babf39b", + "folders/moved-not-revived": "a557a1d88431da1b", "folders/watch-every-listed": "9b481488d7a7fba1", "folders/watch-every-names-folder": "edaf9c03beca0ac5", "folders/watch-every-own-server": "7185aaf2cc305107", @@ -2628,7 +2630,7 @@ "folders/confirm-returned-file": "5e63672ae8718177", "folders/confirm-uncertain-file": "49d3ff8eda66751e", "folders/confirm-uncertain-file-unsure-count": "5b58a6f52c1853ce", - "folders/confirm-deletes-queued": "939c8322dc9a0555", + "folders/confirm-deletes-queued": "0d0a811b56754c44", "folders/confirm-pull-removes": "f468356d14ab2a44", "folders/confirm-pull-removes-removed-count": "32477b40ddea1fce", "folders/restore-disk-files": "3d2479d0ff5671c9", @@ -3321,7 +3323,7 @@ "items/create-malformed-type": "75faf07479567aca", "items/create-unknown-type": "1cb37252ab035e16", "items/create-type-not-permitted": "85d5ed5444d6288f", - "items/system-types": "8b1a9dcf14cb5b30", + "items/system-types": "dba35e98f17c9bea", "items/get": "3f2c17ffa756a4c6", "items/get-matches-list": "5bf4317800b13625", "items/found-after-create": "2e17726458055587", @@ -3363,7 +3365,7 @@ "items/write-race-key-narrowed-entry": "4d4e055ea3f61cb5", "items/write-race-key-revoked": "4b0d88d84dbdb7ad", "items/write-race-key-revoked-entry": "cda8803de0e7b5d1", - "items/idempotency-operations": "97297200747ead08", + "items/idempotency-operations": "968cf2b38a619ab8", "items/idempotency-replay": "b4a5251badf68216", "items/idempotency-once": "42bae408261e141d", "items/idempotency-reused": "b866942bd4645957", @@ -5062,7 +5064,7 @@ "types/source-filter-missing-source": "ffaa4dc35611173f", "types/source-filter-by-id": "6617d24512e658f0", "types/shipped": "a3d618d635c669ae", - "types/shipped-system": "6cfe61ef22c357cd", + "types/shipped-system": "aa1b148e069da6e4", "types/message": "8002937a17b40e06", "types/highlight": "14a1b11a8d1a648f", "types/entity": "699d3bbbab2cdc19", @@ -5240,6 +5242,34 @@ "versions/edge-version-first": "587bca8c43e279d8", "versions/edge-stale-bare": "fbf0437e4ea551a3", "versions/edge-update-race": "c641d1c3ed00b57e" + }, + "views": { + "views/create": "959566ca51ca25e7", + "views/settings": "85b636d00a956fcf", + "views/layout-parts": "f70483d70e133f3b", + "views/kind-options": "04c7e7e23f26508f", + "views/field": "02114bde3f58d547", + "views/option-fields": "055d14c971d5fd52", + "views/announced": "b72c4295caa0af9b", + "views/setting-checked": "5bc61f670459c9ac", + "views/setting-invalid": "d153f570006683a8", + "views/setting-unknown-type": "884ca8fbfed8757b", + "views/setting-missing": "56ebe0ee833a3400", + "views/setting-undeclared": "b1ad34741abb962a", + "views/change": "733147767b7bd86a", + "views/change-stale": "fed5a73b872a87fa", + "views/change-conflict": "7a79133177b6d9ab", + "views/change-refused": "0d246ea8a65fff2d", + "views/change-empty": "8fbd71af79a4efd4", + "views/change-revoked": "9372b83117c3031a", + "views/change-revoked-unchanged": "92403c00a55e0a1c", + "views/not-found": "53f43b426f7ef6e2", + "views/parent-deleted": "05f5861c8a7adfac", + "views/delete": "141a7a4357b639d1", + "views/delete-announced": "dcbedf915038670d", + "views/grant": "a9eab891de3269d9", + "views/read": "263f3ff144383c04", + "views/read-hidden": "f505cc50dc77f05d" } } } diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index 9e47133c..f35cedd3 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.28"} +{"version":"v0.0.29"} diff --git a/src/generated/server-image.md b/src/generated/server-image.md index a857845d..f8a1ab39 100644 --- a/src/generated/server-image.md +++ b/src/generated/server-image.md @@ -1,5 +1,5 @@ ```bash -docker pull ghcr.io/withmarfa/marfa-server:0.0.28 +docker pull ghcr.io/withmarfa/marfa-server:0.0.29 docker run --name marfa -d --restart unless-stopped -p 127.0.0.1:8600:8600 \ - -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.28 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.29 ``` diff --git a/src/lib/docs-map.test.ts b/src/lib/docs-map.test.ts index 880d2250..c1c74e6b 100644 --- a/src/lib/docs-map.test.ts +++ b/src/lib/docs-map.test.ts @@ -54,7 +54,7 @@ describe('buildDocsMap', () => { '- Core types', ...['Bookmark', 'Entity', 'Event', 'File', 'Highlight', 'Media', 'Message', 'Note', 'Task'].map((title) => ` - ${title}]`), '- System types', - ...['Connection', 'Folder'].map((title) => ` - ${title}]`), + ...['Connection', 'Folder', 'View'].map((title) => ` - ${title}]`), '- Type rules]', '- Field kinds]', ]);