diff --git a/.github/scripts/sync-from-marfa.test.sh b/.github/scripts/sync-from-marfa.test.sh
index 13c3afcf..90a3330d 100644
--- a/.github/scripts/sync-from-marfa.test.sh
+++ b/.github/scripts/sync-from-marfa.test.sh
@@ -320,7 +320,7 @@ catalog_json="$(cat <<'EOF'
"title": { "type": "string", "description": "A title | or not" },
"language": { "type": "string", "description": "A language", "format": "bcp47" }
},
- "display_hints": { "title_field": "title", "body_field": "body" },
+ "display_hints": { "title_field": "title", "body_field": "body", "icon": "sticky-note" },
"merge_policy": { "fields": { "body": "keep_both_copies" }, "default": "last_writer_wins" }
},
{
@@ -336,7 +336,7 @@ catalog_json="$(cat <<'EOF'
"level": { "type": "enum", "description": "How urgent", "enum_values": ["low", "high"] },
"to": { "type": "array", "description": "Who it is for", "items_type": "string" }
},
- "display_hints": { "title_field": "title", "body_field": "body" },
+ "display_hints": { "icon": "notebook-pen" },
"merge_policy": { "fields": { "body": "keep_both_copies" }, "default": "last_writer_wins" },
"roles": ["container"]
},
@@ -409,8 +409,9 @@ note_expected="$(cat <<'EOF'
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `body` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `body` |
+| [`icon`](/catalog/your-own-types#display-hints) | `sticky-note` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body`; `last_writer_wins` for every other field |
## Subtypes
@@ -419,9 +420,10 @@ note_expected="$(cat <<'EOF'
A memo (for a team) with a level.
-| Setting | Value |
-| ---------------------------------------- | ----------- |
-| [`roles`](/catalog/your-own-types#roles) | `container` |
+| Setting | Value |
+| ----------------------------------------------- | -------------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `notebook-pen` |
+| [`roles`](/catalog/your-own-types#roles) | `container` |
| Field | Kind | Required | Description |
| ------- | ------------------- | -------- | ---------------------------------- |
@@ -1169,7 +1171,7 @@ check "a subtype equal to its parent says nothing of hints or merge policy" \
"$(printf '### core.note.same [#core-note-same]\n\nSays what the parent says.')" \
"$(sed -n '/^### core.note.same/,/^### core.note.drift/p' "${drift_file}" | sed '$d')"
check "a subtype that differs says its hints and merge policy as values" \
- "$(printf '%s\n%s\n%s' '| [`title_field`](/catalog/your-own-types#fields) | `language` |' '| [`body_field`](/catalog/your-own-types#fields) | `body` |' '| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for every field |')" \
+ "$(printf '%s\n%s' '| [`title_field`](/catalog/your-own-types#display-hints) | `language` |' '| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for every field |')" \
"$(sed -n '/^### core.note.drift/,$p' "${drift_file}" | grep '^| \[`')"
check "the parent says its own hints once, and a subtype equal to it adds none" 1 \
"$(grep -cE '^\| \[`title_field`\].*\| `title` +\|$' "${drift_file}")"
diff --git a/content/docs/(overview)/items-and-types.mdx b/content/docs/(overview)/items-and-types.mdx
index 01180410..59c55f90 100644
--- a/content/docs/(overview)/items-and-types.mdx
+++ b/content/docs/(overview)/items-and-types.mdx
@@ -44,6 +44,10 @@ When your type should be readable as another type without taking on its properti
+## How a type is shown
+
+A type also tells an app how to show its items, through three **display hints**: the field that names an item, the field that holds its text, and an icon. **The text is Markdown**: an app renders it, and a device reads the links in it to make [edges](/properties-metadata-and-edges). The icon is a Lucide name such as `calendar`, and the owner can choose another for any type, including the ones Marfa ships. Each hint comes from the nearest type that sets it, so a child that sets only an icon keeps its parent's title and text. [Type rules](/catalog/your-own-types#display-hints) has the details.
+
## Where an item came from
Every item records its **source**, the program that created it, such as `laptop` or `todoist`: the creating key's own source, or one that key claims. It never changes. An item can also carry a `source_id`, its own ID at that source, such as a task's ID in Todoist. Together they identify one record, so a connector that reads the record again updates the item instead of making a second one. [Manage keys](/run/manage-keys#let-keys-share-a-source) shows how keys share a source.
diff --git a/content/docs/build/change-a-type.mdx b/content/docs/build/change-a-type.mdx
index f57233e8..141747f3 100644
--- a/content/docs/build/change-a-type.mdx
+++ b/content/docs/build/change-a-type.mdx
@@ -37,6 +37,10 @@ let normalized = try await api.check(itemType: definition)
The answer's `type` is your definition with its defaults made explicit, and `registered_type` is the registered one in the same form. Compare those two, not `resolved_type`, which adds what the registered type inherits. Both registered forms are `null` when the identifier isn't registered. An unknown schema key, anywhere in a definition, is refused `400 invalid_schema`.
+**A replacement keeps the icon the owner chose** for the type (see [display hints](/catalog/your-own-types#display-hints)), and `registered_type` shows the icon you registered, not the owner's.
+
+**A `GET` of a type, put back as a replacement, stores only what you wrote.** A hint the type doesn't set itself (`title_field`, `body_field` or `icon`) and that you send back with the value the `GET` answered is the type's inheritance read back, not your choice. Marfa stores no value for it, so the type goes on following its parent and the owner's icon. A hint the type sets stays, including one set to a different value than its parent's. For an icon the type sets, a value equal to the owner's icon keeps the icon you registered. A hint you newly set to the value the type already inherits is stored as none, and the type reads the same.
+
Then replace the type:
```bash tab="CLI" tab-group="client"
diff --git a/content/docs/build/connector-pull-and-push.mdx b/content/docs/build/connector-pull-and-push.mdx
index 788195bd..ee1a0df5 100644
--- a/content/docs/build/connector-pull-and-push.mdx
+++ b/content/docs/build/connector-pull-and-push.mdx
@@ -64,6 +64,8 @@ An item's natural key is its `source`, which comes from the connector's key, and
`atomic: false` writes the records that pass when one is refused, and reports that one as `errored`. By default, one refused record **rolls the whole batch back**.
+**Write an item's text as Markdown.** The field its type names in [`display_hints.body_field`](/catalog/your-own-types#display-hints) holds Markdown, and Marfa stores what you send without checking it. A vendor that returns HTML needs the connector to convert it first, or an app shows the tags.
+
## Write only what changed
A write of the same properties an item already holds **still raises its `version`** and announces an `item.updated` event. A pull that rewrites every record on every run therefore makes noise for everyone who follows the event stream.
diff --git a/content/docs/build/connector.mdx b/content/docs/build/connector.mdx
index ad47d0c0..1be678e4 100644
--- a/content/docs/build/connector.mdx
+++ b/content/docs/build/connector.mdx
@@ -121,11 +121,11 @@ A connector writes items of a type of its own. Save its definition beside the co
"description": "The entry's ID in the reading list service."
}
},
- "display_hints": { "title_field": "title" }
+ "display_hints": { "icon": "bookmark-plus" }
}
```
-Its parent is `core.bookmark`, so an entry already has a `title`, a `url` and an `author`. The type adds only the entry's ID in the service. [Define a type](/build/define-a-type) explains each part.
+Its parent is `core.bookmark`, so an entry already has a `title`, a `url` and an `author`, and shows by the title and text its parent names. The type adds the entry's ID in the service and an icon, which the owner can replace with another. [Define a type](/build/define-a-type) explains each part.
diff --git a/content/docs/build/define-a-type.mdx b/content/docs/build/define-a-type.mdx
index f677e9de..6a60b653 100644
--- a/content/docs/build/define-a-type.mdx
+++ b/content/docs/build/define-a-type.mdx
@@ -113,6 +113,8 @@ Marfa made the label from the identifier, and `provenance.created_by` names who
`marfa types get app.example.receipt` (`GET /types/{id}`) answers the type with the properties it inherits, such as `blob_ref`, `mime_type`, `width` and `height`. An item must hold **every required property along the chain**, so a receipt needs a `blob_ref`, a `mime_type` and a `merchant`. A write that lacks one is refused `400 invalid_properties`, naming it.
+A type can also set [`display_hints`](/catalog/your-own-types#display-hints): the field that names an item, the field that holds its Markdown text, and a Lucide `icon`. Each one comes from the nearest type that sets it, so a receipt that sets only an icon keeps the title its parent names.
+
Once the type is registered, [change or delete it](/build/change-a-type) as your app grows.
## What your key needs
diff --git a/content/docs/catalog/(types)/your-own-types.mdx b/content/docs/catalog/(types)/your-own-types.mdx
index 695b47f4..33698181 100644
--- a/content/docs/catalog/(types)/your-own-types.mdx
+++ b/content/docs/catalog/(types)/your-own-types.mdx
@@ -1,6 +1,6 @@
---
title: Type rules
-description: "The rules a type you register must meet: its identifier, fields, parent, roles, merge policy and retention policy."
+description: "The rules a type you register must meet: its identifier, fields, parent, display hints, roles, merge policy and retention policy."
---
**Marfa refuses a definition that breaks a rule with `400 invalid_schema`** unless the rule names another answer. [Define a type](/build/define-a-type) shows how to register one.
@@ -28,7 +28,7 @@ description: "The rules a type you register must meet: its identifier, fields, p
| `maxLength`, `maxItems` | A whole number of at least 1. `maxLength` applies to `string` and `enum`, `maxItems` to `array`. |
| Name | Not one of the reserved field names below. Refused `400 property_shadows_field`. |
| `thumbnail` | At most one, counting inherited ones. Not named `title`, `body`, `description` or `name`, and not an array's `items_type`. |
-| `title_field`, `body_field` | Name a field the type declares or inherits, and not a `thumbnail` field. |
+| `title_field`, `body_field` | Name a field the type declares or inherits, and not a `thumbnail` field. See [Display hints](#display-hints). |
**A write that leaves a wrong element in an `array` field** is refused `400 invalid_properties`, and `details.errors` names the field and position, as `names.1`.
@@ -56,6 +56,36 @@ description: "The rules a type you register must meet: its identifier, fields, p
A replacement or deletion that breaks another type's claim is refused the same way, and `details.dependent_type_ids` names the claimants. A new target that isn't a platform type needs write on it, as a `parent` does.
+## Display hints
+
+`display_hints` says how an app shows an item of the type. It takes three optional hints:
+
+| Hint | What it holds | Marfa refuses |
+| --- | --- | --- |
+| `title_field` | The name of the field that gives an item its name. | A field the type and its ancestors don't declare, or a `thumbnail` field. |
+| `body_field` | The name of the field that holds the item's text. **The value is Markdown.** Apps render it, and a device reads it to make [edges](/catalog/edges) from the links it contains. Marfa stores the value as sent and doesn't check it. | The same as `title_field`. |
+| `icon` | The name of a [Lucide](https://lucide.dev/icons/) icon in kebab-case, such as `star` or `file-music`. | A name outside Lucide 1.61.0, the one release Marfa pins: a different case, a name from a later release, or an icon Lucide removed. **Use the canonical name, not an alias:** `home` is refused and `house` is accepted. Refused `400 invalid_schema`, naming `display_hints.icon`. |
+
+Every operation that takes a type definition applies the same rule, including [normalize a type definition](/api-reference/types/normalizeType). The names Marfa accepts are in the [generated list](https://github.com/withmarfa/marfa/blob/main/packages/types/generated/lucide-icons.json).
+
+**Each hint comes from the nearest type that sets it:** the type itself, then its parent, then that type's parent. A type that sets only an `icon` still shows the title and body its parent names. `GET /types/{id}` answers each hint after that choice, and `GET /types` lists the hints each type declares itself, with the owner's icon in place of the author's.
+
+### The owner's icon
+
+**The owner can choose the icon of any type**, including the types Marfa ships and a connector's types, without replacing the definition:
+
+```bash
+marfa types set-icon user.recipe chef-hat
+marfa types clear-icon user.recipe
+```
+
+The same two operations are [set a type's icon](/api-reference/types/setTypeIcon) and [clear a type's icon](/api-reference/types/clearTypeIcon).
+
+- **The choice sits beside the definition.** Replacing the type, as a connector does each time it starts, keeps it, and deleting the type discards it.
+- **Reads answer the owner's icon** in place of the author's. A subtype with no icon of its own shows its parent's, and one with its own keeps it unless the owner chose another for that subtype.
+- **Devices take it the next time they read the catalog:** at a catch-up, or when a follow next opens its stream. Marfa sends no event for a change to a type, so a device shows the old icon until then.
+- **Who can change it:** the owner, a local process, or a key with `schema.write` and write on the type in its type map. A key without `schema.write` is refused `403 forbidden`, and one without write on the type `403 type_not_permitted`. A connector's key can change the icon only if it holds both.
+
## Roles
`roles` takes `container` alone. A `container` type can be the target of an [`in-collection`](/catalog/edges#in-collection) edge.
diff --git a/content/docs/run/export-and-restore.mdx b/content/docs/run/export-and-restore.mdx
index 308a49dc..45377747 100644
--- a/content/docs/run/export-and-restore.mdx
+++ b/content/docs/run/export-and-restore.mdx
@@ -3,7 +3,7 @@ title: Export and restore an archive
description: Export your items, edges, types and files to an archive, and restore it into the same instance or another, such as a new build.
---
-An archive carries your content to another instance: items with their tags, extensions and earlier versions, edges, the types and edge types you registered, and file bytes. It leaves the instance's own setup behind, so it holds no keys, webhooks, configuration, audit log or owner's picture. It can move your data to a new build when you [upgrade](/run/upgrade), but **test the restore on fresh storage first**: a different build may refuse the archive.
+An archive carries your content to another instance: items with their tags, extensions and earlier versions, edges, the types and edge types you registered, the icons you chose for types, and file bytes. It leaves the instance's own setup behind, so it holds no keys, webhooks, configuration, audit log or owner's picture. It can move your data to a new build when you [upgrade](/run/upgrade), but **test the restore on fresh storage first**: a different build may refuse the archive.
An archive isn't a long-term backup, because only the build that wrote it is sure to read it. To recover an instance from a failed disk, [back up the data directory](/run/back-up-and-restore) instead.
@@ -158,7 +158,10 @@ marfa --socket /srv/marfa/data/control/marfa.sock restore marfa-archive.tar.gz
"types_registered": 0,
"types_skipped": 0,
"edge_types_registered": 0,
- "edge_types_skipped": 0
+ "edge_types_skipped": 0,
+ "type_icons_added": 0,
+ "type_icons_skipped": 0,
+ "type_icons_skipped_reasons": {}
}
```
@@ -168,6 +171,7 @@ The same archive restored again answers `"imported": 0`, `"duplicates": 6` and `
- **`blobs_missing`** lists the blobs the archive names but carries no bytes for, and no store of the instance holds. The restore still writes the items but no record for those blobs, so their files have no bytes until you [upload them again](#when-an-archive-lacks-a-files-bytes). The CLI then exits with code 1 and `blobs_missing`.
- **`edges_skipped_reasons`** counts why edges were skipped, such as `already_present` or `endpoint_missing`. An edge to an item in the trash is restored.
+- **`type_icons_added`, `type_icons_skipped` and `type_icons_skipped_reasons`** count the icons you chose for types (see [the owner's icon](/catalog/your-own-types#the-owners-icon)). A restore adds an icon only for a type that has none from you yet, so it never replaces a choice you made on the instance since the export, and counts that skip as `already_set`. An icon that isn't a name in the pinned Lucide release, such as one from a build that pins a later release, is skipped and counted as `unknown_icon`, and the rest of the archive restores. A restore refuses the whole archive if an icon line is damaged, or names a type that neither the archive nor the instance registers.
A restore writes in one transaction. If it's refused, fails or is interrupted, it leaves nothing behind, so you can run it again. Other writes wait while it runs, so let it answer before you start your apps.
diff --git a/content/docs/run/upgrade.mdx b/content/docs/run/upgrade.mdx
index bb23b88e..39697e4b 100644
--- a/content/docs/run/upgrade.mdx
+++ b/content/docs/run/upgrade.mdx
@@ -64,7 +64,7 @@ marfa --socket /srv/marfa/data/control/marfa.sock restore marfa-archive.tar.gz
### Recreate what an archive doesn't hold
-Make your keys again, register your webhooks again, set the configuration again and upload the owner's picture again: **an archive carries none of them**. Sign your apps in again too. Restored app connections are revoked historical records, and their items carry `restored:` sources rather than the sources of the apps you reconnect. [Manage keys](/run/manage-keys), [Settings](/run/settings) and [Change the owner's profile](/run/sign-in-as-the-owner#change-the-owners-profile) cover them.
+An archive carries the icons you chose for types, so those need no step here. Make your keys again, register your webhooks again, set the configuration again and upload the owner's picture again: **an archive carries none of them**. Sign your apps in again too. Restored app connections are revoked historical records, and their items carry `restored:` sources rather than the sources of the apps you reconnect. [Manage keys](/run/manage-keys), [Settings](/run/settings) and [Change the owner's profile](/run/sign-in-as-the-owner#change-the-owners-profile) cover them.
diff --git a/openapi.json b/openapi.json
index 5c1d52c9..6755b80a 100644
--- a/openapi.json
+++ b/openapi.json
@@ -890,11 +890,15 @@
},
"body_field": {
"type": "string",
- "description": "The field that holds an item's body for display."
+ "description": "The field that holds an item's body for display. The field holds Markdown."
+ },
+ "icon": {
+ "type": "string",
+ "description": "The icon that stands for the type, as the kebab-case name of an icon in the Lucide release Marfa pins, such as `calendar-days`. On a type read back, the owner's own choice replaces the author's."
}
},
"additionalProperties": {},
- "description": "Which fields of a type clients show as an item's title and body."
+ "description": "How clients show a type and its items: the fields that hold an item's title and body, and the type's icon. Each hint comes from the nearest type that sets it."
},
"VersionPolicy": {
"type": "object",
@@ -3956,7 +3960,7 @@
"$ref": "#/components/schemas/DisplayHints"
},
{
- "description": "Which fields clients show as an item's title and body."
+ "description": "How clients show the type and its items: the title field, the body field and the icon."
}
]
},
@@ -4105,7 +4109,7 @@
"type": "null"
}
],
- "description": "The canonical registered definition, with inherited fields, policies and roles resolved as on `GET /types/{id}`; `null` if its identifier is not registered."
+ "description": "The canonical registered definition, with inherited fields, policies and roles resolved as on `GET /types/{id}`, with the author's icon and not the owner's; `null` if its identifier is not registered."
}
},
"required": [
@@ -4250,7 +4254,7 @@
"$ref": "#/components/schemas/DisplayHints"
},
{
- "description": "Which fields clients show as an item's title and body."
+ "description": "How clients show the type and its items: the title field, the body field and the icon."
}
]
},
@@ -4488,7 +4492,7 @@
"$ref": "#/components/schemas/DisplayHints"
},
{
- "description": "Which fields clients show as an item's title and body."
+ "description": "How clients show the type and its items: the title field, the body field and the icon."
}
]
},
@@ -4547,6 +4551,56 @@
"additionalProperties": false,
"description": "The schema that replaces a type's stored one."
},
+ "InvalidSchemaOrValidationErrorRefusal": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string",
+ "enum": [
+ "invalid_schema",
+ "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."
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "description": "What went wrong."
+ }
+ },
+ "required": [
+ "error"
+ ],
+ "description": "An error response."
+ },
+ "TypeIconRequest": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "type": "string",
+ "description": "The kebab-case name of an icon in the Lucide release Marfa pins, such as `calendar-days`."
+ }
+ },
+ "required": [
+ "icon"
+ ],
+ "additionalProperties": false,
+ "description": "The icon to show for a type."
+ },
"TypeHasSubtypesOrTypeInUseRefusal": {
"type": "object",
"properties": {
@@ -26500,12 +26554,732 @@
}
}
},
- "403": {
- "description": "- `type_not_permitted`: your credential reaches no type, or `type` names a type you can't read with none readable under it.\n- `edge_permission_denied`: the filter has an `edge` or `backref` term for an edge type you can't read.",
+ "403": {
+ "description": "- `type_not_permitted`: your credential reaches no type, or `type` names a type you can't read with none readable under it.\n- `edge_permission_denied`: the filter has an `edge` or `backref` term for an edge type you can't read.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EdgePermissionDeniedOrTypeNotPermittedRefusal"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/types": {
+ "get": {
+ "operationId": "listTypes",
+ "tags": [
+ "Types"
+ ],
+ "summary": "List types",
+ "description": "Returns every type on this instance: those Marfa ships and those registered through `POST /types`. Every credential reads the whole list, whatever its type map reaches.",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns every type. A type lists only its own fields, so resolve the rest through `parent`.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeDefinitionPage"
+ }
+ }
+ },
+ "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-Marfa-Reach": {
+ "$ref": "#/components/headers/X-Marfa-Reach"
+ }
+ }
+ },
+ "400": {
+ "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ValidationErrorRefusal"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "409": {
+ "description": "`reach_changed`: the reach in `X-Marfa-Reach` has changed. Rebuild the working copy.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReachChangedRefusal"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "X-Marfa-Reach",
+ "in": "header",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "minLength": 64,
+ "maxLength": 64
+ },
+ "description": "The reach from a copy stream, for a working copy. Marfa reads the current data and checks the reach in one snapshot, and returns `409 reach_changed` if it has changed. Leave it out for an ordinary read."
+ }
+ ]
+ },
+ "post": {
+ "operationId": "registerType",
+ "tags": [
+ "Types"
+ ],
+ "summary": "Register a type",
+ "description": "Registers a type under the `app.*`, `user.*` or `.*` namespace and returns it. Name a `parent` to inherit its fields.",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeDefinitionInput"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Returns the new type.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeResponse"
+ }
+ }
+ },
+ "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": "- `missing_required_field`: `fields` is missing.\n- `validation_error`: `id` is malformed, or `parent` isn't registered or makes too deep a chain.\n- `invalid_schema`: the schema is invalid or has a key it doesn't define (`details.errors` names each path).\n- `property_shadows_field`: a field is named like one every item has.\n- `inheritance_violation`: the type reshapes an inherited field.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InheritanceViolationOrInvalidSchemaOrMissingRequiredFieldOrPropertyShadowsFieldOrValidationErrorRefusal"
+ }
+ }
+ },
+ "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": "- `forbidden`: you don't have `metadata.types:write`, or the identifier is under `core.*`, `system.*` or `marfa.*`, which no credential can register.\n- `type_not_permitted`: your type map lacks write on the identifier or a non-platform `parent` or `compatible_with` target (`details.grant` names it).",
+ "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"
+ }
+ }
+ },
+ "409": {
+ "description": "- `type_already_exists`: a type has this identifier.\n- `link_taken`: two items that a forced delete left under this identifier hold the same value in the `link_field`.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LinkTakenOrTypeAlreadyExistsRefusal"
+ },
+ {
+ "$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"
+ }
+ }
+ },
+ "422": {
+ "description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CompatibleWithViolationRefusal"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "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/EdgePermissionDeniedOrTypeNotPermittedRefusal"
+ "$ref": "#/components/schemas/WriteContentionRefusal"
}
}
},
@@ -26530,12 +27304,12 @@
}
}
},
- "429": {
- "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.",
+ "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/RateLimitedRefusal"
+ "$ref": "#/components/schemas/InsufficientStorageRefusal"
}
}
},
@@ -26557,48 +27331,56 @@
},
"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"
- }
- }
+ }
+ },
+ "parameters": [
+ {
+ "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}$"
},
- "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.",
+ "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."
+ }
+ ]
+ }
+ },
+ "/types/{id}": {
+ "get": {
+ "operationId": "getType",
+ "tags": [
+ "Types"
+ ],
+ "summary": "Get a type",
+ "description": "Returns a type's schema, with the fields and policies it inherits from its ancestors. Every credential reads any type, whatever its type map reaches.",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string",
+ "description": "The identifier of the type, such as `core.note`."
+ },
+ "required": true,
+ "description": "The identifier of the type, such as `core.note`.",
+ "name": "id",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Returns the type.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WriteContentionRefusal"
+ "$ref": "#/components/schemas/RegisteredType"
}
}
},
@@ -26617,18 +27399,15 @@
},
"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.",
+ "400": {
+ "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InsufficientStorageRefusal"
+ "$ref": "#/components/schemas/ValidationErrorRefusal"
}
}
},
@@ -26652,60 +27431,13 @@
"$ref": "#/components/headers/X-Error-Code"
}
}
- }
- }
- }
- },
- "/types": {
- "get": {
- "operationId": "listTypes",
- "tags": [
- "Types"
- ],
- "summary": "List types",
- "description": "Returns every type on this instance: those Marfa ships and those registered through `POST /types`. Every credential reads the whole list, whatever its type map reaches.",
- "security": [
- {
- "bearerAuth": []
- }
- ],
- "responses": {
- "200": {
- "description": "Returns every type. A type lists only its own fields, so resolve the rest through `parent`.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/TypeDefinitionPage"
- }
- }
- },
- "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-Marfa-Reach": {
- "$ref": "#/components/headers/X-Marfa-Reach"
- }
- }
},
- "400": {
- "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.",
+ "401": {
+ "description": "`unauthorized`: the request has no credential, or its credential is not valid.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ValidationErrorRefusal"
+ "$ref": "#/components/schemas/UnauthorizedRefusal"
}
}
},
@@ -26730,12 +27462,12 @@
}
}
},
- "401": {
- "description": "`unauthorized`: the request has no credential, or its credential is not valid.",
+ "404": {
+ "description": "`type_not_found`: no type has this identifier.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UnauthorizedRefusal"
+ "$ref": "#/components/schemas/TypeNotFoundRefusal"
}
}
},
@@ -26761,11 +27493,11 @@
}
},
"409": {
- "description": "`reach_changed`: the reach in `X-Marfa-Reach` has changed. Rebuild the working copy.",
+ "description": "`type_chain_unresolvable`: the type's parent chain is circular or too deep to resolve. `PUT /types/{id}` can correct it.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ReachChangedRefusal"
+ "$ref": "#/components/schemas/TypeChainUnresolvableRefusal"
}
}
},
@@ -26913,47 +27645,55 @@
}
}
}
- },
- "parameters": [
- {
- "name": "X-Marfa-Reach",
- "in": "header",
- "required": false,
- "schema": {
- "type": "string",
- "pattern": "^[0-9a-f]{64}$",
- "minLength": 64,
- "maxLength": 64
- },
- "description": "The reach from a copy stream, for a working copy. Marfa reads the current data and checks the reach in one snapshot, and returns `409 reach_changed` if it has changed. Leave it out for an ordinary read."
- }
- ]
+ }
},
- "post": {
- "operationId": "registerType",
+ "put": {
+ "operationId": "replaceType",
"tags": [
"Types"
],
- "summary": "Register a type",
- "description": "Registers a type under the `app.*`, `user.*` or `.*` namespace and returns it. Name a `parent` to inherit its fields.",
+ "summary": "Replace a type",
+ "description": "Replaces a registered type's whole schema and returns it. A field you leave out is removed, but items keep their values for it. `metadata.types:write` alone can add optional fields and change `label`, `description`, `display_hints` or `version`.",
"security": [
{
"bearerAuth": []
}
],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string",
+ "description": "The identifier of the type, such as `acme.deal`."
+ },
+ "required": true,
+ "description": "The identifier of the type, such as `acme.deal`.",
+ "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/TypeDefinitionInput"
+ "$ref": "#/components/schemas/TypeDefinitionUpdate"
}
}
}
},
"responses": {
- "201": {
- "description": "Returns the new type.",
+ "200": {
+ "description": "Returns the replaced type.",
"content": {
"application/json": {
"schema": {
@@ -26980,7 +27720,7 @@
}
},
"400": {
- "description": "- `missing_required_field`: `fields` is missing.\n- `validation_error`: `id` is malformed, or `parent` isn't registered or makes too deep a chain.\n- `invalid_schema`: the schema is invalid or has a key it doesn't define (`details.errors` names each path).\n- `property_shadows_field`: a field is named like one every item has.\n- `inheritance_violation`: the type reshapes an inherited field.",
+ "description": "- `missing_required_field`: `fields` is missing.\n- `validation_error`: `id` is malformed, or `parent` isn't registered or makes a bad chain.\n- `invalid_schema`: the schema is invalid or has a key it doesn't define (`details.errors` names each path).\n- `property_shadows_field`: a field has the name of one every item has.\n- `inheritance_violation`: a field's shape differs in a parent or subtype.",
"content": {
"application/json": {
"schema": {
@@ -27040,11 +27780,41 @@
}
},
"403": {
- "description": "- `forbidden`: you don't have `metadata.types:write`, or the identifier is under `core.*`, `system.*` or `marfa.*`, which no credential can register.\n- `type_not_permitted`: your type map lacks write on the identifier or a non-platform `parent` or `compatible_with` target (`details.grant` names it).",
+ "description": "- `forbidden`: you lack `schema.write` and `metadata.types:write`, or `details.changes` names changes needing `schema.write`.\n- `core_type_immutable`: Marfa ships this type.\n- `type_not_permitted`: your type map lacks write on the type or a new non-platform `parent` or `compatible_with` target (`details.grant` names it).",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal"
+ "$ref": "#/components/schemas/CoreTypeImmutableOrForbiddenOrTypeNotPermittedRefusal"
+ }
+ }
+ },
+ "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": "`type_not_found`: no type has this identifier.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeNotFoundRefusal"
}
}
},
@@ -27070,13 +27840,13 @@
}
},
"409": {
- "description": "- `type_already_exists`: a type has this identifier.\n- `link_taken`: two items that a forced delete left under this identifier hold the same value in the `link_field`.",
+ "description": "`link_taken`: the replacement names a `link_field` in which two items of the type, in any state, hold the same value.",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
- "$ref": "#/components/schemas/LinkTakenOrTypeAlreadyExistsRefusal"
+ "$ref": "#/components/schemas/LinkTakenRefusal"
},
{
"$ref": "#/components/schemas/InstanceChangedRefusal"
@@ -27128,7 +27898,7 @@
}
},
"422": {
- "description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy.",
+ "description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy, or the change breaks another type's claim (`details.dependent_type_ids`).",
"content": {
"application/json": {
"schema": {
@@ -27280,53 +28050,66 @@
}
}
}
- },
- "parameters": [
- {
- "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."
- }
- ]
- }
- },
- "/types/{id}": {
- "get": {
- "operationId": "getType",
+ }
+ },
+ "delete": {
+ "operationId": "deleteType",
"tags": [
"Types"
],
- "summary": "Get a type",
- "description": "Returns a type's schema, with the fields and policies it inherits from its ancestors. Every credential reads any type, whatever its type map reaches.",
+ "summary": "Delete a type",
+ "description": "Deletes a type no item holds and no folder, view or webhook names. With `force=true`, a key keeps the items without their type and leaves the settings as they are; new writes then fail until it's registered again.",
"security": [
{
"bearerAuth": []
+ },
+ {
+ "ownerSession": []
}
],
"parameters": [
{
"schema": {
"type": "string",
- "description": "The identifier of the type, such as `core.note`."
+ "description": "The identifier of the type, such as `acme.deal`."
},
"required": true,
- "description": "The identifier of the type, such as `core.note`.",
+ "description": "The identifier of the type, such as `acme.deal`.",
"name": "id",
"in": "path"
+ },
+ {
+ "schema": {
+ "type": "string",
+ "enum": [
+ "true",
+ "false"
+ ],
+ "description": "Delete the type even if items of it exist, in any state, or a folder, view or webhook names it. It doesn't cover subtypes or compatibility dependents."
+ },
+ "required": false,
+ "description": "Delete the type even if items of it exist, in any state, or a folder, view or webhook names it. It doesn't cover subtypes or compatibility dependents.",
+ "name": "force",
+ "in": "query"
+ },
+ {
+ "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 type.",
+ "description": "Returns `ok: true`.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RegisteredType"
+ "$ref": "#/components/schemas/Ok"
}
}
},
@@ -27408,6 +28191,36 @@
}
}
},
+ "403": {
+ "description": "- `forbidden`: you lack `schema.write`, your owner sign-in is too old, or direct authority requested `force=true`.\n- `core_type_immutable`: Marfa ships this type, and no credential can delete it.\n- `type_not_permitted`: your type map doesn't grant write on the type.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CoreTypeImmutableOrForbiddenOrTypeNotPermittedRefusal"
+ }
+ }
+ },
+ "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": "`type_not_found`: no type has this identifier.",
"content": {
@@ -27439,11 +28252,69 @@
}
},
"409": {
- "description": "`type_chain_unresolvable`: the type's parent chain is circular or too deep to resolve. `PUT /types/{id}` can correct it.",
+ "description": "- `type_has_subtypes`: other types name this one as their `parent` (`details.subtype_ids` lists them). Delete them, or give them another parent with `PUT /types/{id}`.\n- `type_in_use`: an item of the type exists, in any state, or a folder, view or webhook names it, and `force` isn't `true`. `details` counts the items and lists the settings. Move them with `POST /types/{id}/move`.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TypeChainUnresolvableRefusal"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/TypeHasSubtypesOrTypeInUseRefusal"
+ },
+ {
+ "$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"
+ }
+ }
+ },
+ "422": {
+ "description": "`compatible_with_violation`: other types name this type in `compatible_with`. `details.dependent_type_ids` lists them. Remove their claims before deleting the target; `force=true` does not bypass this check.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CompatibleWithViolationRefusal"
}
}
},
@@ -27592,14 +28463,16 @@
}
}
}
- },
- "put": {
- "operationId": "replaceType",
+ }
+ },
+ "/types/{id}/move": {
+ "post": {
+ "operationId": "moveType",
"tags": [
"Types"
],
- "summary": "Replace a type",
- "description": "Replaces a registered type's whole schema and returns it. A field you leave out is removed, but items keep their values for it. `metadata.types:write` alone can add optional fields and change `label`, `description`, `display_hints` or `version`.",
+ "summary": "Move items to another type",
+ "description": "Moves every item of the type you can see, in any state, to another type as a bulk-action job. Once none remains, it rewrites the folders, views and webhooks that name the type. `dry_run: true` returns what it would do.",
"security": [
{
"bearerAuth": []
@@ -27609,10 +28482,10 @@
{
"schema": {
"type": "string",
- "description": "The identifier of the type, such as `acme.deal`."
+ "description": "The identifier of the type to move items out of, such as `acme.deal`."
},
"required": true,
- "description": "The identifier of the type, such as `acme.deal`.",
+ "description": "The identifier of the type to move items out of, such as `acme.deal`.",
"name": "id",
"in": "path"
},
@@ -27632,18 +28505,64 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TypeDefinitionUpdate"
+ "type": "object",
+ "properties": {
+ "to": {
+ "type": "string",
+ "description": "The identifier of the type to move items to, such as `acme.contract`. It must be registered."
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": {},
+ "description": "Properties to merge into every item as it moves, such as a field the new type requires. A `null` value removes a property."
+ },
+ "dry_run": {
+ "type": "boolean",
+ "description": "`true` returns the items and settings the move would change and writes nothing. Defaults to `false`."
+ }
+ },
+ "required": [
+ "to"
+ ],
+ "additionalProperties": false
}
}
}
},
"responses": {
"200": {
- "description": "Returns the replaced type.",
+ "description": "With `dry_run: true`, returns what the move would do.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TypeResponse"
+ "$ref": "#/components/schemas/TypeMoveDryRun"
+ }
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "202": {
+ "description": "Returns the queued job. Poll it with `GET /items/bulk-actions/jobs/{id}`. Its `result` lists the first 100 items it couldn't move in `errors`, counts the items of the type still left in `remaining` and `remaining_hidden`, and says in `settings_rewritten` whether it rewrote `settings`. Run the move again to move what remains.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BulkActionJob"
}
}
},
@@ -27666,11 +28585,11 @@
}
},
"400": {
- "description": "- `missing_required_field`: `fields` is missing.\n- `validation_error`: `id` is malformed, or `parent` isn't registered or makes a bad chain.\n- `invalid_schema`: the schema is invalid or has a key it doesn't define (`details.errors` names each path).\n- `property_shadows_field`: a field has the name of one every item has.\n- `inheritance_violation`: a field's shape differs in a parent or subtype.",
+ "description": "- `missing_required_field`: `to` is missing.\n- `validation_error`: `to` is malformed or names the type itself.\n- `unknown_type`: no type has the identifier `to`.\n- `bulk_cap_exceeded`: the type holds over 50,000 items.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InheritanceViolationOrInvalidSchemaOrMissingRequiredFieldOrPropertyShadowsFieldOrValidationErrorRefusal"
+ "$ref": "#/components/schemas/BulkCapExceededOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal"
}
}
},
@@ -27726,11 +28645,11 @@
}
},
"403": {
- "description": "- `forbidden`: you lack `schema.write` and `metadata.types:write`, or `details.changes` names changes needing `schema.write`.\n- `core_type_immutable`: Marfa ships this type.\n- `type_not_permitted`: your type map lacks write on the type or a new non-platform `parent` or `compatible_with` target (`details.grant` names it).",
+ "description": "- `core_type_immutable`: Marfa ships the type, and its items stay in it.\n- `type_not_permitted`: your type map doesn't grant write on both types.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CoreTypeImmutableOrForbiddenOrTypeNotPermittedRefusal"
+ "$ref": "#/components/schemas/CoreTypeImmutableOrTypeNotPermittedRefusal"
}
}
},
@@ -27786,13 +28705,13 @@
}
},
"409": {
- "description": "`link_taken`: the replacement names a `link_field` in which two items of the type, in any state, hold the same value.",
+ "description": "`type_move_blocked`: a folder, view or webhook names the type in a way the move can't rewrite. `details.settings` lists each, with its `reason`. Change them, then move.",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
- "$ref": "#/components/schemas/LinkTakenRefusal"
+ "$ref": "#/components/schemas/TypeMoveBlockedRefusal"
},
{
"$ref": "#/components/schemas/InstanceChangedRefusal"
@@ -27843,36 +28762,6 @@
}
}
},
- "422": {
- "description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy, or the change breaks another type's claim (`details.dependent_type_ids`).",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CompatibleWithViolationRefusal"
- }
- }
- },
- "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"
- }
- }
- },
"429": {
"description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.",
"content": {
@@ -27997,65 +28886,38 @@
}
}
}
- },
- "delete": {
- "operationId": "deleteType",
+ }
+ },
+ "/types/normalize": {
+ "post": {
+ "operationId": "normalizeType",
"tags": [
"Types"
],
- "summary": "Delete a type",
- "description": "Deletes a type no item holds and no folder, view or webhook names. With `force=true`, a key keeps the items without their type and leaves the settings as they are; new writes then fail until it's registered again.",
+ "summary": "Normalize a type definition",
+ "description": "Normalizes a candidate definition and returns its declared form, the registered declared definition and the registered resolved definition. It does not register or replace a type.",
"security": [
{
"bearerAuth": []
- },
- {
- "ownerSession": []
}
],
- "parameters": [
- {
- "schema": {
- "type": "string",
- "description": "The identifier of the type, such as `acme.deal`."
- },
- "required": true,
- "description": "The identifier of the type, such as `acme.deal`.",
- "name": "id",
- "in": "path"
- },
- {
- "schema": {
- "type": "string",
- "enum": [
- "true",
- "false"
- ],
- "description": "Delete the type even if items of it exist, in any state, or a folder, view or webhook names it. It doesn't cover subtypes or compatibility dependents."
- },
- "required": false,
- "description": "Delete the type even if items of it exist, in any state, or a folder, view or webhook names it. It doesn't cover subtypes or compatibility dependents.",
- "name": "force",
- "in": "query"
- },
- {
- "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/TypeDefinitionInput"
+ }
+ }
}
- ],
+ },
"responses": {
"200": {
- "description": "Returns `ok: true`.",
+ "description": "Returns the normalized candidate and the registered definitions.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Ok"
+ "$ref": "#/components/schemas/NormalizedType"
}
}
},
@@ -28078,11 +28940,11 @@
}
},
"400": {
- "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.",
+ "description": "- `missing_required_field`: `fields` is missing.\n- `validation_error`: `id` is malformed, or `parent` isn't registered or makes too deep a chain.\n- `invalid_schema`: the schema is invalid or has a key it doesn't define (`details.errors` names each path).\n- `property_shadows_field`: a field is named like one every item has.\n- `inheritance_violation`: the type reshapes an inherited field.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ValidationErrorRefusal"
+ "$ref": "#/components/schemas/InheritanceViolationOrInvalidSchemaOrMissingRequiredFieldOrPropertyShadowsFieldOrValidationErrorRefusal"
}
}
},
@@ -28137,74 +28999,14 @@
}
}
},
- "403": {
- "description": "- `forbidden`: you lack `schema.write`, your owner sign-in is too old, or direct authority requested `force=true`.\n- `core_type_immutable`: Marfa ships this type, and no credential can delete it.\n- `type_not_permitted`: your type map doesn't grant write on the type.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CoreTypeImmutableOrForbiddenOrTypeNotPermittedRefusal"
- }
- }
- },
- "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": "`type_not_found`: no type has this identifier.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/TypeNotFoundRefusal"
- }
- }
- },
- "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": "- `type_has_subtypes`: other types name this one as their `parent` (`details.subtype_ids` lists them). Delete them, or give them another parent with `PUT /types/{id}`.\n- `type_in_use`: an item of the type exists, in any state, or a folder, view or webhook names it, and `force` isn't `true`. `details` counts the items and lists the settings. Move them with `POST /types/{id}/move`.",
+ "description": "`type_chain_unresolvable`: the type's parent chain is circular or too deep to resolve. `PUT /types/{id}` can correct it.",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
- "$ref": "#/components/schemas/TypeHasSubtypesOrTypeInUseRefusal"
+ "$ref": "#/components/schemas/TypeChainUnresolvableRefusal"
},
{
"$ref": "#/components/schemas/InstanceChangedRefusal"
@@ -28256,7 +29058,7 @@
}
},
"422": {
- "description": "`compatible_with_violation`: other types name this type in `compatible_with`. `details.dependent_type_ids` lists them. Remove their claims before deleting the target; `force=true` does not bypass this check.",
+ "description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy.",
"content": {
"application/json": {
"schema": {
@@ -28408,30 +29210,45 @@
}
}
}
- }
+ },
+ "parameters": [
+ {
+ "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."
+ }
+ ]
}
},
- "/types/{id}/move": {
- "post": {
- "operationId": "moveType",
+ "/types/{id}/icon": {
+ "put": {
+ "operationId": "setTypeIcon",
"tags": [
"Types"
],
- "summary": "Move items to another type",
- "description": "Moves every item of the type you can see, in any state, to another type as a bulk-action job. Once none remains, it rewrites the folders, views and webhooks that name the type. `dry_run: true` returns what it would do.",
+ "summary": "Set a type's icon",
+ "description": "Sets the owner's icon for a type. Reads answer it in place of the author's, and a subtype with no icon inherits it. Replacing the type keeps it. Needs `schema.write` and write on the type, as replacing it does.",
"security": [
{
"bearerAuth": []
+ },
+ {
+ "ownerSession": []
}
],
"parameters": [
{
"schema": {
"type": "string",
- "description": "The identifier of the type to move items out of, such as `acme.deal`."
+ "description": "The identifier of the type, such as `core.note` or `acme.deal`."
},
"required": true,
- "description": "The identifier of the type to move items out of, such as `acme.deal`.",
+ "description": "The identifier of the type, such as `core.note` or `acme.deal`.",
"name": "id",
"in": "path"
},
@@ -28451,37 +29268,18 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "to": {
- "type": "string",
- "description": "The identifier of the type to move items to, such as `acme.contract`. It must be registered."
- },
- "properties": {
- "type": "object",
- "additionalProperties": {},
- "description": "Properties to merge into every item as it moves, such as a field the new type requires. A `null` value removes a property."
- },
- "dry_run": {
- "type": "boolean",
- "description": "`true` returns the items and settings the move would change and writes nothing. Defaults to `false`."
- }
- },
- "required": [
- "to"
- ],
- "additionalProperties": false
+ "$ref": "#/components/schemas/TypeIconRequest"
}
}
}
},
"responses": {
"200": {
- "description": "With `dry_run: true`, returns what the move would do.",
+ "description": "Returns the type, with the icon.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/TypeMoveDryRun"
+ "$ref": "#/components/schemas/TypeResponse"
}
}
},
@@ -28503,12 +29301,12 @@
}
}
},
- "202": {
- "description": "Returns the queued job. Poll it with `GET /items/bulk-actions/jobs/{id}`. Its `result` lists the first 100 items it couldn't move in `errors`, counts the items of the type still left in `remaining` and `remaining_hidden`, and says in `settings_rewritten` whether it rewrote `settings`. Run the move again to move what remains.",
+ "400": {
+ "description": "`invalid_schema`: `icon` isn't the name of an icon in the pinned Lucide release (`details.errors` names it).\n- `validation_error`: the query has a parameter this endpoint doesn't take.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BulkActionJob"
+ "$ref": "#/components/schemas/InvalidSchemaOrValidationErrorRefusal"
}
}
},
@@ -28527,15 +29325,18 @@
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
+ },
+ "X-Error-Code": {
+ "$ref": "#/components/headers/X-Error-Code"
}
}
},
- "400": {
- "description": "- `missing_required_field`: `to` is missing.\n- `validation_error`: `to` is malformed or names the type itself.\n- `unknown_type`: no type has the identifier `to`.\n- `bulk_cap_exceeded`: the type holds over 50,000 items.",
+ "401": {
+ "description": "`unauthorized`: the request has no credential, or its credential is not valid.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BulkCapExceededOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal"
+ "$ref": "#/components/schemas/UnauthorizedRefusal"
}
}
},
@@ -28560,12 +29361,123 @@
}
}
},
- "401": {
- "description": "`unauthorized`: the request has no credential, or its credential is not valid.",
+ "403": {
+ "description": "- `forbidden`: you lack `schema.write`, or your key is a connector's, which cannot change what the owner chose.\n- `type_not_permitted`: your type map doesn't grant write on the type.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UnauthorizedRefusal"
+ "$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": "`type_not_found`: no type has this identifier.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TypeNotFoundRefusal"
+ }
+ }
+ },
+ "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"
}
}
},
@@ -28587,15 +29499,18 @@
},
"X-Error-Code": {
"$ref": "#/components/headers/X-Error-Code"
+ },
+ "Retry-After": {
+ "$ref": "#/components/headers/Retry-After"
}
}
},
- "403": {
- "description": "- `core_type_immutable`: Marfa ships the type, and its items stay in it.\n- `type_not_permitted`: your type map doesn't grant write on both types.",
+ "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/CoreTypeImmutableOrTypeNotPermittedRefusal"
+ "$ref": "#/components/schemas/InternalErrorRefusal"
}
}
},
@@ -28620,12 +29535,12 @@
}
}
},
- "404": {
- "description": "`type_not_found`: no type has this identifier.",
+ "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/TypeNotFoundRefusal"
+ "$ref": "#/components/schemas/WriteContentionRefusal"
}
}
},
@@ -28650,19 +29565,12 @@
}
}
},
- "409": {
- "description": "`type_move_blocked`: a folder, view or webhook names the type in a way the move can't rewrite. `details.settings` lists each, with its `reason`. Change them, then move.",
+ "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": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TypeMoveBlockedRefusal"
- },
- {
- "$ref": "#/components/schemas/InstanceChangedRefusal"
- }
- ]
+ "$ref": "#/components/schemas/InsufficientStorageRefusal"
}
}
},
@@ -28686,67 +29594,53 @@
"$ref": "#/components/headers/X-Error-Code"
}
}
+ }
+ }
+ },
+ "delete": {
+ "operationId": "clearTypeIcon",
+ "tags": [
+ "Types"
+ ],
+ "summary": "Clear a type's icon",
+ "description": "Removes the icon the owner set for a type, so reads show the author's again. Clearing a type with no owner icon succeeds and changes nothing.",
+ "security": [
+ {
+ "bearerAuth": []
},
- "413": {
- "description": "`request_too_large`: the request body is larger than this instance accepts.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RequestTooLargeRefusal"
- }
- }
+ {
+ "ownerSession": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string",
+ "description": "The identifier of the type, such as `core.note` or `acme.deal`."
},
- "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"
- }
- }
+ "required": true,
+ "description": "The identifier of the type, such as `core.note` or `acme.deal`.",
+ "name": "id",
+ "in": "path"
},
- "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"
- }
- }
+ {
+ "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}$"
},
- "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.",
+ "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 type, with the author's icon if it has one.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InternalErrorRefusal"
+ "$ref": "#/components/schemas/TypeResponse"
}
}
},
@@ -28765,18 +29659,15 @@
},
"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.",
+ "400": {
+ "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WriteContentionRefusal"
+ "$ref": "#/components/schemas/ValidationErrorRefusal"
}
}
},
@@ -28801,12 +29692,12 @@
}
}
},
- "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.",
+ "401": {
+ "description": "`unauthorized`: the request has no credential, or its credential is not valid.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InsufficientStorageRefusal"
+ "$ref": "#/components/schemas/UnauthorizedRefusal"
}
}
},
@@ -28830,67 +29721,13 @@
"$ref": "#/components/headers/X-Error-Code"
}
}
- }
- }
- }
- },
- "/types/normalize": {
- "post": {
- "operationId": "normalizeType",
- "tags": [
- "Types"
- ],
- "summary": "Normalize a type definition",
- "description": "Normalizes a candidate definition and returns its declared form, the registered declared definition and the registered resolved definition. It does not register or replace a type.",
- "security": [
- {
- "bearerAuth": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/TypeDefinitionInput"
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Returns the normalized candidate and the registered definitions.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/NormalizedType"
- }
- }
- },
- "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": "- `missing_required_field`: `fields` is missing.\n- `validation_error`: `id` is malformed, or `parent` isn't registered or makes too deep a chain.\n- `invalid_schema`: the schema is invalid or has a key it doesn't define (`details.errors` names each path).\n- `property_shadows_field`: a field is named like one every item has.\n- `inheritance_violation`: the type reshapes an inherited field.",
+ "403": {
+ "description": "- `forbidden`: you lack `schema.write`, or your key is a connector's, which cannot change what the owner chose.\n- `type_not_permitted`: your type map doesn't grant write on the type.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InheritanceViolationOrInvalidSchemaOrMissingRequiredFieldOrPropertyShadowsFieldOrValidationErrorRefusal"
+ "$ref": "#/components/schemas/ForbiddenOrTypeNotPermittedRefusal"
}
}
},
@@ -28915,12 +29752,12 @@
}
}
},
- "401": {
- "description": "`unauthorized`: the request has no credential, or its credential is not valid.",
+ "404": {
+ "description": "`type_not_found`: no type has this identifier.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UnauthorizedRefusal"
+ "$ref": "#/components/schemas/TypeNotFoundRefusal"
}
}
},
@@ -28946,18 +29783,11 @@
}
},
"409": {
- "description": "`type_chain_unresolvable`: the type's parent chain is circular or too deep to resolve. `PUT /types/{id}` can correct it.",
+ "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/TypeChainUnresolvableRefusal"
- },
- {
- "$ref": "#/components/schemas/InstanceChangedRefusal"
- }
- ]
+ "$ref": "#/components/schemas/InstanceChangedRefusal"
}
}
},
@@ -29003,36 +29833,6 @@
}
}
},
- "422": {
- "description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CompatibleWithViolationRefusal"
- }
- }
- },
- "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"
- }
- }
- },
"429": {
"description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.",
"content": {
@@ -29156,19 +29956,7 @@
}
}
}
- },
- "parameters": [
- {
- "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."
- }
- ]
+ }
}
},
"/edge-types": {
@@ -57980,6 +58768,21 @@
"edge_types_skipped": {
"type": "number",
"description": "How many edge types Marfa skipped because the instance already registers them identically."
+ },
+ "type_icons_added": {
+ "type": "number",
+ "description": "How many of the owner's type icons Marfa added, for types the instance had no owner icon for."
+ },
+ "type_icons_skipped": {
+ "type": "number",
+ "description": "How many of the owner's type icons Marfa skipped."
+ },
+ "type_icons_skipped_reasons": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "number"
+ },
+ "description": "How many of the owner's type icons Marfa skipped for each reason: `already_set` when the instance already has an owner icon for the type, `unknown_icon` when the name is not an icon of the Lucide release Marfa pins."
}
},
"required": [
@@ -57993,7 +58796,10 @@
"types_registered",
"types_skipped",
"edge_types_registered",
- "edge_types_skipped"
+ "edge_types_skipped",
+ "type_icons_added",
+ "type_icons_skipped",
+ "type_icons_skipped_reasons"
]
}
}
@@ -58490,7 +59296,9 @@
"replaceType",
"deleteType",
"moveType",
- "normalizeType"
+ "normalizeType",
+ "setTypeIcon",
+ "clearTypeIcon"
]
},
{
diff --git a/scripts/sync-from-marfa.sh b/scripts/sync-from-marfa.sh
index 30dd564b..7cdf9345 100755
--- a/scripts/sync-from-marfa.sh
+++ b/scripts/sync-from-marfa.sh
@@ -530,7 +530,7 @@ STRATEGIES = {"last_writer_wins", "keep_both_copies"}
CARDINALITIES = {"one-to-one", "one-to-many", "many-to-one", "many-to-many"}
CASCADES = {"orphan", "cascade"}
ROLES = {"container"}
-HINT_KEYS = ("title_field", "body_field")
+HINT_KEYS = ("title_field", "body_field", "icon")
RULES = "/catalog/your-own-types"
@@ -636,11 +636,11 @@ def render_types(types):
def settings(t, parent):
"""The definition's keys other than its fields. A subtype gives only those its parent does not."""
rows = []
- hints, parent_hints = hints_of(t), (hints_of(parent) if parent else None)
- if hints is not None and hints != parent_hints:
- for key in HINT_KEYS:
- if key in hints:
- rows.append([f"[{code(key)}]({RULES}#fields)", code(hints[key])])
+ hints, parent_hints = hints_of(t), (hints_of(parent) if parent else None) or {}
+ for key in HINT_KEYS:
+ # A hint comes from the nearest type that sets it, so a subtype says only the ones it changes.
+ if hints is not None and key in hints and hints[key] != parent_hints.get(key):
+ rows.append([f"[{code(key)}]({RULES}#display-hints)", code(hints[key])])
policy = t.get("merge_policy")
if isinstance(policy, dict) and (parent is None or policy != parent.get("merge_policy")):
rows.append([f"[{code('merge_policy')}]({RULES}#merge-policy)", merge_policy_value(t["id"], policy)])
diff --git a/src/generated/GLOSSARY.md b/src/generated/GLOSSARY.md
index bee225a7..3ca95e96 100644
--- a/src/generated/GLOSSARY.md
+++ b/src/generated/GLOSSARY.md
@@ -12,15 +12,17 @@ The words are grouped by topic.
### Items and types
-| Word | What it means |
-| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **item** | One record. The unit everything else hangs off: it has a type, a state, properties, and a time it occurred at. A tier too, except on `system.*` items, where the dimension does not apply. |
-| **type** | What an item is, named in the namespace grammar, such as `core.note`, `user.recipe` or `.`. A type under `app.*`, `user.*` or a publisher root is registered at run time and enforced, and keeps the `version` its registration names; `core.*` and `system.*` are shipped by the platform and immutable, and `marfa.*` is reserved to it. |
-| **state** | Where an item sits in its lifecycle: `active`, `archived`, `trashed`, `revoked`. Lists and searches default to `active`; `state=any` reads every state in one pass. An export is a copy rather than a listing and carries every state but `trashed`. A connector's **state document** is another thing, the JSON object it keeps on the instance under its source; name it in full. |
-| **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 sort and group say in what order and how they group. How an app shows them is the app's own. Written only through `/views`. Not a folder, which holds items as files on a machine. |
+| Word | What it means |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **item** | One record. The unit everything else hangs off: it has a type, a state, properties, and a time it occurred at. A tier too, except on `system.*` items, where the dimension does not apply. |
+| **type** | What an item is, named in the namespace grammar, such as `core.note`, `user.recipe` or `.`. A type under `app.*`, `user.*` or a publisher root is registered at run time and enforced, and keeps the `version` its registration names; `core.*` and `system.*` are shipped by the platform and immutable, and `marfa.*` is reserved to it. |
+| **display hints** | What a type says about how to show its items: `title_field`, the field that names an item, `body_field`, the field that holds its text as Markdown, and `icon`. Each hint comes from the nearest type, the type itself first and then its parents in turn, that sets it. |
+| **icon** | The picture a type is shown by: a kebab-case name from one pinned release of Lucide. A type's author sets it as `display_hints.icon`; the owner can choose another for any type, and Marfa keeps that choice beside the definition, so a connector that registers its types again does not undo it. |
+| **state** | Where an item sits in its lifecycle: `active`, `archived`, `trashed`, `revoked`. Lists and searches default to `active`; `state=any` reads every state in one pass. An export is a copy rather than a listing and carries every state but `trashed`. A connector's **state document** is another thing, the JSON object it keeps on the instance under its source; name it in full. |
+| **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 sort and group say in what order and how they group. How an app shows them is the app's own. Written only through `/views`. Not a folder, which holds items as files on a machine. |
### Properties, metadata and edges
@@ -182,20 +184,20 @@ These name things that were removed. None of them belongs in a tracked file, in
A permission is named by what it permits, never by a noun for the whole. The mapping is fixed here so the derived names are as settled as the three the vocabulary decided.
-| Permission | What it permits |
-| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `schema.write` | Removing type and edge type definitions that already exist, and replacing them, only those the credential's type or edge map grants write on. Registering one is separately fenced by `metadata.types:write` and `metadata.edge_types:write`. The one act both permissions open is a type's own evolution through `PUT /types/{id}`: adding an optional field no stored row holds a value under, and changing its label, description, display hints and version. Every other replacement, a field's removal among them, and every deletion takes `schema.write`. |
-| `keys.mint` | Minting keys within the caller's current access. It also permits listing, changing and revoking keys within that access. An app may delegate only what its grant permits. |
-| `items.purge` | Destroying a trashed row irrecoverably, through the single-row operation and the bulk one alike. |
-| `webhooks.manage` | Reading, registering, changing and removing the outbound subscriptions that send an instance's events out, and redelivering what they sent. Reading takes the same permission as registering. |
-| `config.manage` | Reading and replacing the instance configuration at `/config`: the enforcement levers and the cleanup-job retention overrides. **Not `config.write`**: a caller refused it cannot read the config either. |
-| `audit.read` | Reading the audit log. There is no `audit.write` beside it: the server writes the log, a caller never does. |
-| `grants.manage` | Listing and revoking other apps' access. Deliberately not `keys.mint`: a key is this app's own credential, a grant is another app's, and folding them together would let an app trusted to rotate a key revoke every other app the owner has authorized. |
-| `instance.read` | Reading detailed health, metrics, housekeeping status, retired platform types, blob storage diagnostics and bulk job status. It grants no content read access. |
-| `instance.maintain` | Running housekeeping, resetting platform type definitions and cancelling bulk jobs. It grants no content write access. |
-| `connectors.manage` | Reading and administering connector registrations, their runs and their endpoints, deleting registrations and clearing a registration's retained state. Heartbeats, holds, run reports, reading and writing retained state, agreements and received deliveries belong to the connector's own key alone. |
-| `blobs.manage` | Managing blob bytes and copies across the instance, without granting item content access. |
-| `keys.manage` | Listing and revoking keys across the instance, and changing their metadata or narrowing access. It does not grant unrestricted minting or widening; changing enforcement overrides also requires `config.manage`. |
+| Permission | What it permits |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `schema.write` | Removing type and edge type definitions that already exist, and replacing them, only those the credential's type or edge map grants write on. Registering one is separately fenced by `metadata.types:write` and `metadata.edge_types:write`. The one act both permissions open is a type's own evolution through `PUT /types/{id}`: adding an optional field no stored row holds a value under, and changing its label, description, display hints and version. Every other replacement, a field's removal among them, and every deletion takes `schema.write`. It also permits changing or clearing the owner's icon for a type, with write on that type in the key's map, the same as to replace the type. |
+| `keys.mint` | Minting keys within the caller's current access. It also permits listing, changing and revoking keys within that access. An app may delegate only what its grant permits. |
+| `items.purge` | Destroying a trashed row irrecoverably, through the single-row operation and the bulk one alike. |
+| `webhooks.manage` | Reading, registering, changing and removing the outbound subscriptions that send an instance's events out, and redelivering what they sent. Reading takes the same permission as registering. |
+| `config.manage` | Reading and replacing the instance configuration at `/config`: the enforcement levers and the cleanup-job retention overrides. **Not `config.write`**: a caller refused it cannot read the config either. |
+| `audit.read` | Reading the audit log. There is no `audit.write` beside it: the server writes the log, a caller never does. |
+| `grants.manage` | Listing and revoking other apps' access. Deliberately not `keys.mint`: a key is this app's own credential, a grant is another app's, and folding them together would let an app trusted to rotate a key revoke every other app the owner has authorized. |
+| `instance.read` | Reading detailed health, metrics, housekeeping status, retired platform types, blob storage diagnostics and bulk job status. It grants no content read access. |
+| `instance.maintain` | Running housekeeping, resetting platform type definitions and cancelling bulk jobs. It grants no content write access. |
+| `connectors.manage` | Reading and administering connector registrations, their runs and their endpoints, deleting registrations and clearing a registration's retained state. Heartbeats, holds, run reports, reading and writing retained state, agreements and received deliveries belong to the connector's own key alone. |
+| `blobs.manage` | Managing blob bytes and copies across the instance, without granting item content access. |
+| `keys.manage` | Listing and revoking keys across the instance, and changing their metadata or narrowing access. It does not grant unrestricted minting or widening; changing enforcement overrides also requires `config.manage`. |
These twelve permissions name existing operations. They can be granted to ordinary keys and approved OAuth apps. Content maps remain independent, so holding every named permission does not grant item read or write access. No permission grants private machine authority, owner recovery, access to the direct `/owner` endpoint or archive restore.
diff --git a/src/generated/catalog/builtin-catalog.json b/src/generated/catalog/builtin-catalog.json
index 4077cdbc..b904169d 100644
--- a/src/generated/catalog/builtin-catalog.json
+++ b/src/generated/catalog/builtin-catalog.json
@@ -54,7 +54,8 @@
},
"display_hints": {
"title_field": "title",
- "body_field": "body"
+ "body_field": "body",
+ "icon": "bookmark"
},
"merge_policy": {
"fields": {
@@ -82,7 +83,8 @@
},
"display_hints": {
"title_field": "title",
- "body_field": "description"
+ "body_field": "description",
+ "icon": "layers"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -140,7 +142,8 @@
}
},
"display_hints": {
- "title_field": "name"
+ "title_field": "name",
+ "icon": "shapes"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -240,7 +243,7 @@
}
},
"display_hints": {
- "title_field": "name"
+ "icon": "user"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -333,7 +336,7 @@
}
},
"display_hints": {
- "title_field": "name"
+ "icon": "map-pin"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -424,7 +427,8 @@
},
"display_hints": {
"title_field": "title",
- "body_field": "description"
+ "body_field": "description",
+ "icon": "calendar"
},
"merge_policy": {
"fields": {
@@ -492,7 +496,8 @@
}
},
"display_hints": {
- "title_field": "title"
+ "title_field": "title",
+ "icon": "file"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -562,7 +567,7 @@
}
},
"display_hints": {
- "title_field": "title"
+ "icon": "file-music"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -652,7 +657,7 @@
}
},
"display_hints": {
- "title_field": "title"
+ "icon": "file-image"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -742,7 +747,7 @@
}
},
"display_hints": {
- "title_field": "title"
+ "icon": "file-video-camera"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -808,7 +813,8 @@
}
},
"display_hints": {
- "title_field": "title"
+ "title_field": "title",
+ "icon": "globe"
},
"merge_policy": {
"default": "last_writer_wins"
@@ -864,7 +870,8 @@
},
"display_hints": {
"title_field": "text",
- "body_field": "note"
+ "body_field": "note",
+ "icon": "highlighter"
},
"merge_policy": {
"fields": {
@@ -928,7 +935,8 @@
},
"display_hints": {
"title_field": "title",
- "body_field": "body"
+ "body_field": "body",
+ "icon": "library-big"
},
"merge_policy": {
"fields": {
@@ -1001,8 +1009,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "disc-3"
},
"merge_policy": {
"fields": {
@@ -1078,8 +1085,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "newspaper"
},
"merge_policy": {
"fields": {
@@ -1156,8 +1162,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "book-open"
},
"merge_policy": {
"fields": {
@@ -1253,8 +1258,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "circle-play"
},
"merge_policy": {
"fields": {
@@ -1339,8 +1343,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "film"
},
"merge_policy": {
"fields": {
@@ -1425,8 +1428,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "tv"
},
"merge_policy": {
"fields": {
@@ -1518,8 +1520,7 @@
}
},
"display_hints": {
- "title_field": "title",
- "body_field": "body"
+ "icon": "audio-lines"
},
"merge_policy": {
"fields": {
@@ -1551,7 +1552,8 @@
}
},
"display_hints": {
- "body_field": "body"
+ "body_field": "body",
+ "icon": "message-square"
},
"merge_policy": {
"fields": {
@@ -1586,7 +1588,8 @@
},
"display_hints": {
"title_field": "title",
- "body_field": "body"
+ "body_field": "body",
+ "icon": "sticky-note"
},
"merge_policy": {
"fields": {
@@ -1666,7 +1669,8 @@
},
"display_hints": {
"title_field": "title",
- "body_field": "body"
+ "body_field": "body",
+ "icon": "square-check-big"
},
"merge_policy": {
"fields": {
diff --git a/src/generated/catalog/core.bookmark.md b/src/generated/catalog/core.bookmark.md
index 49490333..f988fa2f 100644
--- a/src/generated/catalog/core.bookmark.md
+++ b/src/generated/catalog/core.bookmark.md
@@ -18,6 +18,7 @@
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `body` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `body` |
+| [`icon`](/catalog/your-own-types#display-hints) | `bookmark` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field |
diff --git a/src/generated/catalog/core.collection.md b/src/generated/catalog/core.collection.md
index f9638f4e..0ec5b494 100644
--- a/src/generated/catalog/core.collection.md
+++ b/src/generated/catalog/core.collection.md
@@ -9,7 +9,8 @@
| Setting | Value |
| ------------------------------------------------------ | ---------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `description` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `description` |
+| [`icon`](/catalog/your-own-types#display-hints) | `layers` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `last_writer_wins` for every field |
| [`roles`](/catalog/your-own-types#roles) | `container` |
diff --git a/src/generated/catalog/core.entity.md b/src/generated/catalog/core.entity.md
index 366f6e9f..6bce5883 100644
--- a/src/generated/catalog/core.entity.md
+++ b/src/generated/catalog/core.entity.md
@@ -17,7 +17,8 @@
| Setting | Value |
| ------------------------------------------------------ | ---------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `name` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `name` |
+| [`icon`](/catalog/your-own-types#display-hints) | `shapes` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `last_writer_wins` for every field |
## Subtypes
@@ -26,6 +27,10 @@
Contact information for an individual. Inherits all core.entity fields.
+| Setting | Value |
+| ----------------------------------------------- | ------ |
+| [`icon`](/catalog/your-own-types#display-hints) | `user` |
+
| Field | Kind | Required | Description |
| -------------- | -------- | -------- | ---------------------------------- |
| `given_name` | `string` | No | Given (first) name |
@@ -44,6 +49,10 @@ Contact information for an individual. Inherits all core.entity fields.
A location or venue. Inherits all core.entity fields.
+| Setting | Value |
+| ----------------------------------------------- | --------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `map-pin` |
+
| Field | Kind | Required | Description |
| ---------------- | -------------------- | -------- | ---------------------------------- |
| `street_address` | `string` | No | Street line |
diff --git a/src/generated/catalog/core.event.md b/src/generated/catalog/core.event.md
index 17b2fa80..4a5aef43 100644
--- a/src/generated/catalog/core.event.md
+++ b/src/generated/catalog/core.event.md
@@ -24,6 +24,7 @@
| Setting | Value |
| ------------------------------------------------------ | ------------------------------------------------------------------------ |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `description` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `description` |
+| [`icon`](/catalog/your-own-types#display-hints) | `calendar` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `notes`; `last_writer_wins` for every other field |
diff --git a/src/generated/catalog/core.file.md b/src/generated/catalog/core.file.md
index 209cf262..91bab5a7 100644
--- a/src/generated/catalog/core.file.md
+++ b/src/generated/catalog/core.file.md
@@ -19,7 +19,8 @@
| Setting | Value |
| ------------------------------------------------------ | ---------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`icon`](/catalog/your-own-types#display-hints) | `file` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `last_writer_wins` for every field |
## Subtypes
@@ -28,6 +29,10 @@
Recordings, music files, voice memos. Inherits all core.file fields.
+| Setting | Value |
+| ----------------------------------------------- | ------------ |
+| [`icon`](/catalog/your-own-types#display-hints) | `file-music` |
+
| Field | Kind | Required | Description |
| ---------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------- |
| `language` | `string` (`bcp47`) | No | BCP 47 language code (for spoken content) |
@@ -37,6 +42,10 @@ Recordings, music files, voice memos. Inherits all core.file fields.
Photos, screenshots, diagrams. Inherits all core.file fields.
+| Setting | Value |
+| ----------------------------------------------- | ------------ |
+| [`icon`](/catalog/your-own-types#display-hints) | `file-image` |
+
| Field | Kind | Required | Description |
| ----------- | ----------- | -------- | ---------------------------------------------------------------------------------------------- |
| `width` | `integer` | No | Width in pixels (server enrichment: derived from the file when the client does not supply it) |
@@ -50,6 +59,10 @@ Photos, screenshots, diagrams. Inherits all core.file fields.
Video files. Inherits all core.file fields.
+| Setting | Value |
+| ----------------------------------------------- | ------------------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `file-video-camera` |
+
| Field | Kind | Required | Description |
| ----------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------- |
| `language` | `string` (`bcp47`) | No | BCP 47 language code (for spoken content) |
@@ -63,3 +76,7 @@ Video files. Inherits all core.file fields.
### core.file.webpage [#core-file-webpage]
An HTML page held as a file, such as a report an agent wrote. Inherits all core.file fields.
+
+| Setting | Value |
+| ----------------------------------------------- | ------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `globe` |
diff --git a/src/generated/catalog/core.highlight.md b/src/generated/catalog/core.highlight.md
index c9074750..c39f966b 100644
--- a/src/generated/catalog/core.highlight.md
+++ b/src/generated/catalog/core.highlight.md
@@ -13,6 +13,7 @@
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `text` |
-| [`body_field`](/catalog/your-own-types#fields) | `note` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `text` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `note` |
+| [`icon`](/catalog/your-own-types#display-hints) | `highlighter` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `note`; `last_writer_wins` for every other field |
diff --git a/src/generated/catalog/core.media.md b/src/generated/catalog/core.media.md
index d424812e..a24f7b33 100644
--- a/src/generated/catalog/core.media.md
+++ b/src/generated/catalog/core.media.md
@@ -18,8 +18,9 @@
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `body` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `body` |
+| [`icon`](/catalog/your-own-types#display-hints) | `library-big` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field |
## Subtypes
@@ -28,9 +29,10 @@
An album. Inherits all core.media fields.
-| Setting | Value |
-| ---------------------------------------- | ----------- |
-| [`roles`](/catalog/your-own-types#roles) | `container` |
+| Setting | Value |
+| ----------------------------------------------- | ----------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `disc-3` |
+| [`roles`](/catalog/your-own-types#roles) | `container` |
| Field | Kind | Required | Description |
| -------------- | --------- | -------- | ------------------------------------- |
@@ -41,6 +43,10 @@ An album. Inherits all core.media fields.
An article. Inherits all core.media fields.
+| Setting | Value |
+| ----------------------------------------------- | ----------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `newspaper` |
+
| Field | Kind | Required | Description |
| ------------ | --------- | -------- | -------------------------- |
| `body` | `string` | No | The article text |
@@ -51,6 +57,10 @@ An article. Inherits all core.media fields.
A book. Inherits all core.media fields.
+| Setting | Value |
+| ----------------------------------------------- | ----------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `book-open` |
+
| Field | Kind | Required | Description |
| ------------ | --------- | -------- | --------------------------------------------- |
| `isbn` | `string` | No | International Standard Book Number (ISO 2108) |
@@ -61,6 +71,10 @@ A book. Inherits all core.media fields.
Any member of an ongoing series: a TV episode, a podcast episode, one part of a serial. Inherits all core.media fields. Joins its series through the in-collection edge, so one episode can belong to several series and outlives any of them.
+| Setting | Value |
+| ----------------------------------------------- | ------------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `circle-play` |
+
| Field | Kind | Required | Description |
| ---------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `medium` | `enum` | No | What the episode is made of. Values: `tv`, `podcast`, `radio`, `video`, `mixed`. |
@@ -74,6 +88,10 @@ Any member of an ongoing series: a TV episode, a podcast episode, one part of a
A film. Inherits all core.media fields.
+| Setting | Value |
+| ----------------------------------------------- | ------ |
+| [`icon`](/catalog/your-own-types#display-hints) | `film` |
+
| Field | Kind | Required | Description |
| ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `duration` | `number` | No | Runtime in seconds |
@@ -86,9 +104,10 @@ A film. Inherits all core.media fields.
Any ongoing media container: a TV show, a podcast, a radio serial, a video series. Inherits all core.media fields. Episodes join it through the in-collection edge; containment is never part of a type name.
-| Setting | Value |
-| ---------------------------------------- | ----------- |
-| [`roles`](/catalog/your-own-types#roles) | `container` |
+| Setting | Value |
+| ----------------------------------------------- | ----------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `tv` |
+| [`roles`](/catalog/your-own-types#roles) | `container` |
| Field | Kind | Required | Description |
| -------- | ------ | -------- | ------------------------------------------------------------------------------------------ |
@@ -99,6 +118,10 @@ Any ongoing media container: a TV show, a podcast, a radio serial, a video serie
A song. Inherits all core.media fields.
+| Setting | Value |
+| ----------------------------------------------- | ------------- |
+| [`icon`](/catalog/your-own-types#display-hints) | `audio-lines` |
+
| Field | Kind | Required | Description |
| -------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `duration` | `number` | No | Track length in seconds |
diff --git a/src/generated/catalog/core.message.md b/src/generated/catalog/core.message.md
index 4312e584..7b6908d4 100644
--- a/src/generated/catalog/core.message.md
+++ b/src/generated/catalog/core.message.md
@@ -10,5 +10,6 @@
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------- |
-| [`body_field`](/catalog/your-own-types#fields) | `body` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `body` |
+| [`icon`](/catalog/your-own-types#display-hints) | `message-square` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body`; `last_writer_wins` for every other field |
diff --git a/src/generated/catalog/core.note.md b/src/generated/catalog/core.note.md
index a8995d41..b08cf4f4 100644
--- a/src/generated/catalog/core.note.md
+++ b/src/generated/catalog/core.note.md
@@ -11,6 +11,7 @@
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `body` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `body` |
+| [`icon`](/catalog/your-own-types#display-hints) | `sticky-note` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field |
diff --git a/src/generated/catalog/core.task.md b/src/generated/catalog/core.task.md
index d21d1480..dd83f73a 100644
--- a/src/generated/catalog/core.task.md
+++ b/src/generated/catalog/core.task.md
@@ -19,6 +19,7 @@
| Setting | Value |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
-| [`title_field`](/catalog/your-own-types#fields) | `title` |
-| [`body_field`](/catalog/your-own-types#fields) | `body` |
+| [`title_field`](/catalog/your-own-types#display-hints) | `title` |
+| [`body_field`](/catalog/your-own-types#display-hints) | `body` |
+| [`icon`](/catalog/your-own-types#display-hints) | `square-check-big` |
| [`merge_policy`](/catalog/your-own-types#merge-policy) | `keep_both_copies` for `body` and `notes`; `last_writer_wins` for every other field |
diff --git a/src/generated/cli-commands.json b/src/generated/cli-commands.json
index a49b42cb..a25f85e5 100644
--- a/src/generated/cli-commands.json
+++ b/src/generated/cli-commands.json
@@ -88,6 +88,8 @@
"types register": {"options": [{"flag": "--file", "value": true}, {"flag": "--body", "value": true}]},
"types normalize": {"options": [{"flag": "--file", "value": true}, {"flag": "--body", "value": true}]},
"types replace": {"options": [{"flag": "--file", "value": true}, {"flag": "--body", "value": true}], "positional": true},
+ "types set-icon": {"options": [], "positional": true},
+ "types clear-icon": {"options": [], "positional": true},
"types delete": {"options": [{"flag": "--force"}], "positional": true},
"types move": {"options": [{"flag": "--dry-run"}, {"flag": "--properties", "value": true}], "positional": true},
"types retired": {"options": []},
diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md
index 671013b6..97a26307 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.39/marfa-0.0.39-darwin-arm64.tar.gz | tar -xz
+curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.40/marfa-0.0.40-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 5a9aea2d..7d9c4935 100644
--- a/src/generated/contract-index.json
+++ b/src/generated/contract-index.json
@@ -495,6 +495,7 @@
"device/hydrate-restart-instance": "dfbeff9f8e29f090",
"device/hydrate-restart-reach": "f3e0dd2213a42542",
"device/hydrate-restart-catalog": "41784240e2ecf297",
+ "device/hydrate-icon-only-catalog": "ab083165d6eb421a",
"device/hydrate-restart-head-aged-out": "38484d0729f6f4d1",
"device/hydrate-restart-head-past-log": "e221b8f38d478ec6",
"device/hydrate-restart-cursor": "20a194db4a75a7fd",
@@ -941,7 +942,8 @@
"device/catalog-replaced-catch-up": "0377b6e5cf959ede",
"device/catalog-replaced-follow": "90f5be33d116a6a7",
"device/catalog-offline": "a6f7a18fec847f81",
- "device/catalog-type-read": "df589ad19049012b",
+ "device/catalog-type-read": "f11969b0fdb676fa",
+ "device/catalog-icon": "8afa9a64443795f5",
"device/catalog-edge-type-read": "c24c69e44f6f6983",
"device/catalog-read-consistent": "ca25ce0516de3858",
"device/catalog-replace-whole": "cec5668a64745365",
@@ -4927,6 +4929,10 @@
"search-and-filters/archive-store-unavailable": "aeccef6ab79c87f7",
"search-and-filters/archive-only": "1f55601a751fc60b",
"search-and-filters/archive-types": "339f0b45f661cff5",
+ "search-and-filters/archive-type-icons": "d71ad9b744cd1e6a",
+ "search-and-filters/restore-type-icons": "fff29826b9a5f04c",
+ "search-and-filters/restore-type-icons-skipped": "4af5ee51c6e1bfff",
+ "search-and-filters/restore-type-icons-refused": "dd7e42ec0439ed7c",
"search-and-filters/archive-history": "f999cd7825a69cd2",
"search-and-filters/archive-history-reach": "bea3add2daa638c4",
"search-and-filters/archive-history-bytes": "f7109371ab90a3ac",
@@ -4952,7 +4958,7 @@
"search-and-filters/archive-selection-key": "b3bdaf7ed2a7df5b",
"search-and-filters/restore-operator": "d7f0f18b82227e9c",
"search-and-filters/restore-roundtrip": "5fb9c51d9951f13f",
- "search-and-filters/restore-counts": "3146e824b7c324a0",
+ "search-and-filters/restore-counts": "33f64ca1c398ffa4",
"search-and-filters/restore-blobs-missing": "6bf4ba1c5be6c593",
"search-and-filters/restore-store-unavailable": "56b419fb7393432b",
"search-and-filters/restore-store-unfinished": "b177de3a49bf6e50",
@@ -5194,7 +5200,10 @@
"types/label-kept": "c22ecb8e0ecef6f4",
"types/read": "64cce0c74507385c",
"types/list-declared-policy": "896dd22f3a40bc9d",
- "types/read-display-hints": "c383e2ca9ed5d100",
+ "types/read-display-hint": "30b182cf9029ba98",
+ "types/list-declared-hints": "4b6a9dfbb8ebaf2c",
+ "types/body-markdown": "141eeedca599b296",
+ "types/icon-name": "95ed6580ac10bfd0",
"types/read-missing": "8af3608faad8ba83",
"types/registry-open": "6035a80609528c95",
"types/parent": "428f972befd06d6f",
@@ -5319,7 +5328,7 @@
"types/platform-remove-missing": "04e8cd32852b7725",
"types/platform-operator-only": "f2df61370e202b21",
"types/normalize-authenticated": "ca834dd1005ac669",
- "types/normalize-answer": "037e99f77c27dfc6",
+ "types/normalize-answer": "f2339c98461d0ce6",
"types/normalize-absent": "a05b32ea25c26809",
"types/normalize-defaults": "5ffaec92f1631ed7",
"types/normalize-declared-refinements": "59f4ccdb93bf17d5",
@@ -5327,7 +5336,19 @@
"types/normalize-no-write": "c6861da0c2c16e85",
"types/normalize-no-audit": "8741f16698fba13d",
"types/normalize-cli": "096eeb37e47c8155",
- "types/item-name-fields": "04b3a851edc17882",
+ "types/icon-set": "953c694f5888defe",
+ "types/icon-clear": "d11609c1d7cdee83",
+ "types/icon-read": "8b5b8daada233290",
+ "types/icon-kept": "02f926b089c3d43c",
+ "types/icon-write-back": "5ceddcc1d064f2bd",
+ "types/hint-write-back": "19d0b63eb6c20d08",
+ "types/icon-refused": "386f3cd4406b8ec8",
+ "types/icon-missing-type": "acb8d8a749fa2d7d",
+ "types/icon-who": "02ea99aeaf49ab48",
+ "types/icon-who-refused": "072df4f49ad439e3",
+ "types/icon-deleted": "b433a7c923351c6b",
+ "types/icon-audit": "b9b01878358698ba",
+ "types/item-name-fields": "75fb65501a6a7dad",
"types/item-name-text": "19ba94664f4e5de8",
"types/item-name-empty": "84c62a6e3746e515",
"types/delete-owner": "72c3a238a5995f6e",
diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json
index bd83e561..018ef023 100644
--- a/src/generated/marfa-release.json
+++ b/src/generated/marfa-release.json
@@ -1 +1 @@
-{"version":"v0.0.39"}
+{"version":"v0.0.40"}
diff --git a/src/generated/server-image.md b/src/generated/server-image.md
index b78f9188..6dca0527 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.39
+docker pull ghcr.io/withmarfa/marfa-server:0.0.40
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.39
+ -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.40
```