From f1ff0c252d32d7e214f36f2de9172fc75c6b223c Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Tue, 20 Oct 2026 03:03:17 +0100 Subject: [PATCH 1/5] chore(sync): sync the docs from marfa v0.0.17 --- openapi.json | 187 ++++++++++++++++++++++-------- src/generated/cli-download.md | 2 +- src/generated/contract-index.json | 74 ++++++++++-- src/generated/error-codes.md | 4 +- src/generated/marfa-release.json | 2 +- src/generated/server-image.md | 4 +- src/generated/settings.md | 1 + 7 files changed, 213 insertions(+), 61 deletions(-) diff --git a/openapi.json b/openapi.json index e99d9f44..c4657862 100644 --- a/openapi.json +++ b/openapi.json @@ -4291,6 +4291,41 @@ ], "description": "A tag, and how many items carry it." }, + "RequestTimeoutRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "request_timeout" + ], + "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." + }, "BlobOrphanPage": { "type": "object", "properties": { @@ -4487,6 +4522,42 @@ ], "description": "An error response." }, + "BlobStoreUnavailableOrWriteContentionRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "blob_store_unavailable", + "write_contention" + ], + "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." + }, "BlobLocationPage": { "type": "object", "properties": { @@ -6573,42 +6644,6 @@ ], "description": "An error response." }, - "BlobStoreUnavailableOrWriteContentionRefusal": { - "type": "object", - "properties": { - "error": { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": [ - "blob_store_unavailable", - "write_contention" - ], - "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." - }, "DriftedPlatformTypePage": { "type": "object", "properties": { @@ -26930,6 +26965,36 @@ } } }, + "408": { + "description": "- `request_timeout`: no bytes arrived for 60 seconds, or for the time `MARFA_UPLOAD_IDLE_TIMEOUT_MS` sets. Marfa stores nothing. An upload has no other time limit while bytes keep arriving.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTimeoutRefusal" + } + } + }, + "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": { @@ -27968,11 +28033,11 @@ } }, "503": { - "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "description": "- `blob_store_unavailable`: no store that holds this blob answered, so Marfa couldn't tell whether it still has the bytes. Try again later.\n- `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" + "$ref": "#/components/schemas/BlobStoreUnavailableOrWriteContentionRefusal" } } }, @@ -41696,7 +41761,7 @@ "Export and restore" ], "summary": "Restore from an archive", - "description": "Restores an exported archive atomically and returns counts of written and skipped records. Other writes wait until it finishes. Requires local authority or the owner, authenticated within five minutes.", + "description": "Restores an exported archive atomically and returns counts of written and skipped records. Other writes wait until it finishes. Requires local authority or the owner, authenticated within five minutes before the request starts.", "security": [ { "ownerSession": [] @@ -41704,7 +41769,7 @@ ], "requestBody": { "required": true, - "description": "The archive file, as `GET /export?format=archive` returned it. Only the build that wrote an archive is sure to read it.", + "description": "The archive file, as `GET /export?format=archive` returned it, with its `manifest.json`, `items.ndjson`, `edges.ndjson` and `types.ndjson`. Only the build that wrote an archive is sure to read it.", "content": { "application/gzip": { "schema": { @@ -41716,7 +41781,7 @@ }, "responses": { "200": { - "description": "Returns the counts. Items and edges keep their IDs, versions and dates, and items keep their tags, extensions and history. Marfa restores an edge whether or not its properties fit its edge type's `property_schema`. Marfa leaves an existing item as it is when an archived one has its ID, natural key or link. An archive holds no keys, webhooks, configuration or tombstones.", + "description": "Returns the counts. Items and edges keep their IDs, versions and dates, and items keep their tags, extensions and history. Marfa restores an edge, and an item of a type you registered, as archived, even if its type now refuses it or is gone. Marfa leaves an existing item as it is when an archived one has its ID, natural key or link. An archive holds no keys, webhooks, configuration or tombstones.", "content": { "application/json": { "schema": { @@ -41808,7 +41873,7 @@ } }, "400": { - "description": "- `validation_error`: the body isn't a valid archive, or is at another format version, repeats an entry, has an invalid row, or has a file over 64 MiB.\n- `invalid_properties`: an item sets a property its type doesn't declare, and `strict_mode` names that type.\n- `invalid_schema`: an archived type is invalid or carries a key the schema doesn't define.", + "description": "- `validation_error`: the body isn't a valid archive, lacks one of its four files (`details.entries`), repeats an entry, is at another version, has an invalid row, or has a file over 64 MiB.\n- `invalid_properties`: a `core` or `system` item breaks its type, an event's schedule can't unfold, or `strict_mode` refuses a property.\n- `invalid_schema`: an archived type is invalid or has an unknown key.", "content": { "application/json": { "schema": { @@ -41868,7 +41933,7 @@ } }, "403": { - "description": "Requires direct owner or local authority and recent owner authentication.", + "description": "Requires direct owner or local authority, and an owner who signed in within five minutes before the request started, however long the upload takes.", "content": { "application/json": { "schema": { @@ -41897,6 +41962,36 @@ } } }, + "408": { + "description": "- `request_timeout`: no bytes of the archive arrived for 60 seconds, or for the time `MARFA_UPLOAD_IDLE_TIMEOUT_MS` sets. Nothing was written. An upload has no other time limit while bytes keep arriving.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTimeoutRefusal" + } + } + }, + "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": "- `conflict`: the archive redefines a type this instance registers differently or registered while the restore ran, carries a core edge type, or carries a snapshot ID that already exists.\n- `link_taken`: the archive registers a type naming a `link_field` in which two rows a forced delete left share a value.", "content": { @@ -43397,7 +43492,7 @@ "Access" ], "summary": "Claim the instance", - "description": "Creates the one owner using a machine-issued setup code or setup-only browser session. The claim, consumed proof, and audit commit together. A claimed instance never reopens setup.", + "description": "Creates the one owner using a machine-issued setup code or setup-only browser session. A claimed instance never reopens setup. A claim from the instance's own origin, which the setup page sends, also signs the owner in with a session cookie.", "requestBody": { "required": true, "content": { @@ -43436,7 +43531,7 @@ }, "responses": { "201": { - "description": "The new owner can sign in.", + "description": "The new owner can sign in. A claim from the instance's own origin also sets the owner session cookie, unless the sign-in that follows the claim fails. A claim with no `Origin` header, such as a script's, sets none.", "content": { "application/json": { "schema": { @@ -46096,7 +46191,7 @@ ], "responses": { "200": { - "description": "With `format=ndjson`, one JSON object per line: `item` and `metadata` for each item, then `edge` for each edge between them whose type you can read. With `format=archive`, a `.tar.gz` of the items with the history you can read, the edges, the type registrations and the blobs they reference that you can read. If an archive fails partway, the connection ends early and the file won't unpack.", + "description": "With `format=ndjson`, one JSON object per line: `item` and `metadata` for each item, then `edge` for each edge between them whose type you can read. With `format=archive`, a `.tar.gz` of the items, history, edges, type registrations and blobs you can read. If the export fails partway or your credential ends or narrows below it, the connection ends early and an archive won't unpack.", "content": { "application/x-ndjson": { "schema": { diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index b76d10db..95f897a7 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.16/marfa-0.0.16-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.17/marfa-0.0.17-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 f13bb164..db330452 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -39,6 +39,9 @@ "blobs/upload-reserve": "f36241433fbe6d9d", "blobs/upload-reserve-streamed": "5dd4d297dd8bc82b", "blobs/upload-reserve-nothing-kept": "4d0aa3ae02f6d5a0", + "blobs/upload-duration": "ef746041b1fcf9b2", + "blobs/upload-idle": "a29a311e620fb115", + "blobs/upload-idle-nothing-kept": "5e5b5a81299e9a5c", "blobs/upload-needs-write": "6b52454e9c775a07", "blobs/upload-grant-registered-later": "d1f4e428026e397f", "blobs/upload-refused-stores-nothing": "6652a3ac479aa438", @@ -1095,6 +1098,7 @@ "edges/inline-empty-list": "887e581c00a26661", "edges/endpoint-source": "208be841d605b0ac", "edges/endpoint-target": "b72dfe145a5648e2", + "edges/endpoint-unregistered": "7502458c33dbaa1a", "edges/endpoint-bulk": "92c9c93f2b1458f3", "edges/endpoint-inline": "43001f00a6e99067", "edges/endpoint-subtype": "62cbd87a598d0542", @@ -1127,6 +1131,8 @@ "edges/cascade-parent-of": "5f8cce4a86abc3c7", "edges/cascade-custom": "e304ddf3e3bc2b45", "edges/orphan-shipped": "0c045bebf46730a5", + "edges/orphan-delete": "1af69fe31e062148", + "edges/orphan-delete-unblocked": "3f1ec0ad263391d7", "edges/block-named": "8c32f76274825a47", "edges/block-hidden-left-out": "a529b229f48eabe4", "edges/block-hidden-refuses": "f7ae7ffc01cad98a", @@ -1178,6 +1184,7 @@ "edges/types-delete-in-use": "5605cc2a96bd6b6f", "edges/types-delete-force": "7088198c53ea63c5", "edges/types-delete-force-keeps": "5fffec5893731cfb", + "edges/orphan-read": "d4c581cbf7609e92", "edges/types-delete-unused": "6d8ccafd2793db80", "edges/types-delete-race": "19fc46d3cc846547", "edges/properties-wrong-type": "e6eb35b45d096b88", @@ -2614,6 +2621,7 @@ "housekeeping/retention-refusal-keeps-config": "0cbfd3dc9627622f", "housekeeping/retention-zero-keeps": "eabda263469326c4", "housekeeping/retention-positive-expires": "fd1948b0bcc243a2", + "housekeeping/retention-trash-keeps-up": "2492c26325e835a1", "housekeeping/retention-maximum-runs": "d720f965d945facf", "housekeeping/retention-setting-maximum-runs": "ae84323821e09c95", "housekeeping/retention-setting-days-range": "0272389dd2a835fa", @@ -2822,8 +2830,12 @@ "instance-claim/replace-browser-proof": "39c4c7f929b634cd", "instance-claim/invalid-owner-details": "1775a407c6d50187", "instance-claim/owner-undeclared-key": "f1b51d0a430ea66f", - "instance-claim/setup-undeclared-key": "577ccdb11ceef908", + "instance-claim/setup-undeclared-key": "330f5e05fc98dd27", "instance-claim/owner-created": "25771e462e229bec", + "instance-claim/claim-signs-in": "d9b9c459c59beeca", + "instance-claim/claim-sign-in-failure": "d31c66f85aa1e61c", + "instance-claim/claim-script-no-session": "d1ac896d61099b4c", + "instance-claim/claim-refused-no-session": "35dc288f5f0b0a3c", "instance-claim/status-fields": "99e082a88f3eb881", "instance-claim/ticket-fields": "e4e860659f8a24be", "instance-claim/claim-closed": "7851ea2f72d2b74d", @@ -2920,7 +2932,7 @@ "instance/health-not-down-200": "f8713c5ca8715445", "instance/health-database-down": "ac8eb07d2cba4aeb", "instance/health-write-down": "984cabf2c592c864", - "instance/health-blob-down": "14593c683c784174", + "instance/health-blob-down": "3cb7a412affb7cce", "instance/health-contention-degraded": "348fcfdb827c8f1e", "instance/health-timeout-degraded": "7ac3719dc8091e0f", "instance/health-disk-down": "f817589934ba2901", @@ -3387,7 +3399,7 @@ "keys-and-oauth/key-unknown": "cc5aa0d3acd83314", "keys-and-oauth/key-malformed-id": "45676aecf585759c", "keys-and-oauth/revoke-answer": "8b230250f4155ac0", - "keys-and-oauth/revoke-immediate": "82be5ab25c5e5159", + "keys-and-oauth/revoke-immediate": "5e92db5a81b98cd4", "keys-and-oauth/revoke-twice": "144cbcd1a99b0221", "keys-and-oauth/revoke-recent": "7418a27515635b88", "keys-and-oauth/revoke-credential-recent": "4fba07b29a99f5f1", @@ -3595,12 +3607,16 @@ "keys-and-oauth/sign-in-address-limit": "a6ee38848c4fb065", "keys-and-oauth/sign-in-form-limit": "0c2a737a7f46839d", "keys-and-oauth/sign-in-form-limit-cookie": "900bf35f839781ee", + "keys-and-oauth/sign-in-form-limit-wait": "7c1e94853427894d", + "keys-and-oauth/sign-in-lockout-page": "707a640ec7a3123f", "keys-and-oauth/sign-in-account-limit": "e780ebdc3e24baa6", "keys-and-oauth/sign-in-other-address": "9a40864c8c0dbf80", "keys-and-oauth/address-ipv6": "82616224f1390b28", "keys-and-oauth/password-change-counted": "ba80f67bb228eb6e", "keys-and-oauth/password-change-limit": "25fc3347738679eb", "keys-and-oauth/device-lookup-address-limit": "709a869d7675f2b8", + "keys-and-oauth/device-lookup-wait": "3ac872da0a71821a", + "keys-and-oauth/device-lookup-page": "516e2ef272bde7d5", "keys-and-oauth/device-lookup-instance-limit": "8c44bf187c9f61dd", "keys-and-oauth/device-lookup-owner-lane": "72a04c00ed59852c", "keys-and-oauth/device-lookup-owner-limit": "b10e397702891606", @@ -4372,7 +4388,10 @@ "search-and-filters/archive-no-later-item": "e18a707d91d9c0c3", "search-and-filters/archive-edge-before": "a8fb855f91388570", "search-and-filters/archive-no-later-edge": "1ca666be96e0463c", - "search-and-filters/archive-restorable": "6dbae5eb69735dd2", + "search-and-filters/archive-restorable": "7de50ffcddbbaa58", + "search-and-filters/archive-rows-shape-free": "76f781f0aa5f1280", + "search-and-filters/archive-edges-unregistered": "a366792a1355644e", + "search-and-filters/archive-edges-end-unregistered": "9ce861a3f7da97ee", "search-and-filters/archive-validation-retry": "5f31c531efc5c91f", "search-and-filters/archive-validation-refused": "365abe404f429603", "search-and-filters/archive-rows-refused": "7fec96e50dcb5b64", @@ -4380,16 +4399,29 @@ "search-and-filters/archive-spool-fault": "d96b0c12e06281e0", "search-and-filters/archive-spool-start": "43b73e3b1f76ecff", "search-and-filters/archive-failed-read": "fe5bbe9525cda271", + "search-and-filters/export-credential-ended": "062133e62eee221d", + "search-and-filters/export-credential-narrowed": "d164acfeb11b05c9", + "search-and-filters/export-narrowed-reaches": "383f6ddf1ee3f93c", + "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-blobs-missing": "6bf4ba1c5be6c593", "search-and-filters/restore-store-unavailable": "56b419fb7393432b", "search-and-filters/restore-store-unavailable-unwritten": "e3eceb6a505a779c", - "search-and-filters/cli-export-lacking": "c42c855be7a1fe2e", + "search-and-filters/cli-export-lacking": "e3e6678962a71354", + "search-and-filters/cli-export-lacking-kept": "0f09765364fdfc21", + "search-and-filters/cli-export-lacking-refused": "ab32d2c92a4fbbfd", + "search-and-filters/cli-export-refused-named": "2f96c179f630d160", + "search-and-filters/cli-export-lacking-path": "5ca6926e6a947a33", + "search-and-filters/cli-export-lacking-named": "3d60d1fb57a736ee", + "search-and-filters/cli-export-lacking-path-state": "1c57cdbf4346e3a7", + "search-and-filters/cli-export-whole": "89268326e44241dd", + "search-and-filters/cli-export-whole-clean": "329cd7a2955bdabf", "search-and-filters/cli-restore-lacking": "e263a4ac77220af7", "search-and-filters/cli-export-cut": "44a507870f0529a3", - "search-and-filters/restore-edge-skips": "d7040dd2f7ca1e54", + "search-and-filters/cli-export-cut-clean": "4926ed6eee035f88", + "search-and-filters/restore-edge-skips": "f506a63d9424a6e4", "search-and-filters/restore-items-kept": "da3e8ce050fe34ec", "search-and-filters/restore-edges-kept": "84dce9e9ac2720ba", "search-and-filters/restore-edges-bin": "52876fb5aeea05c7", @@ -4400,8 +4432,9 @@ "search-and-filters/restore-type-conflict": "af0ea2b603b8f9c4", "search-and-filters/restore-core-edge-type": "e963fd4e927c7a66", "search-and-filters/restore-type-unknown-key": "991655b9dfe38349", - "search-and-filters/restore-nothing": "4ed43e8c29be61de", + "search-and-filters/restore-nothing": "f5856ca633f304a8", "search-and-filters/restore-empty-body": "eb761117bf8e24c0", + "search-and-filters/restore-members": "355949f309450608", "search-and-filters/restore-not-archive": "7ffb014b92efcb68", "search-and-filters/restore-keeps-serving": "aaef6fe2b3e1b7f8", "search-and-filters/restore-version": "5812a317951116ed", @@ -4423,10 +4456,28 @@ "search-and-filters/restore-malformed-held": "911a7b935f2f493c", "search-and-filters/restore-history-exact": "e7bcf507ed3adabc", "search-and-filters/restore-history-shape-free": "6a5142d31ce78893", + "search-and-filters/restore-rows-shape-free": "b132f9be3f1143c9", + "search-and-filters/restore-rows-properties-kept": "a63196eb6314e0ce", + "search-and-filters/restore-schema-version-kept": "04125f268bec7dd5", + "search-and-filters/restore-rows-orphan": "9dd10fe56c347e2d", + "search-and-filters/restore-item-type": "e61ca4ec0979cbf6", + "search-and-filters/restore-item-properties": "77762ac84a3ec19d", + "search-and-filters/restore-edges-unregistered": "3aff807e7e1a2c7e", + "search-and-filters/restore-edges-unregistered-judged": "c93dbfd1a33c42e0", + "search-and-filters/restore-edge-type": "2966e1e24533bdb4", + "search-and-filters/restore-edges-end-unregistered": "a3a70fdd4758195e", + "search-and-filters/restore-edges-end-registered": "6231ca99f3400325", "search-and-filters/restore-snapshot-held": "01137dd259ca44d6", "search-and-filters/restore-strict": "4999c4bc14fdd71d", "search-and-filters/restore-strict-off": "d8f9d2ab4ef25dfe", "search-and-filters/restore-blob-size": "9a094a3c146e8a25", + "search-and-filters/restore-duration": "bba3e2a7b870afd3", + "search-and-filters/restore-idle": "a9c14f7cd8096987", + "search-and-filters/restore-idle-unwritten": "c80e5a96e5f647c0", + "search-and-filters/restore-sign-in-age": "f74f142f93447f1f", + "search-and-filters/restore-sign-in-start": "3875e3b36a4ae6d1", + "search-and-filters/restore-session-ended": "2c3fd2fafcd7dfe6", + "search-and-filters/restore-session-unwritten": "8680ed5c8d81b916", "search-and-filters/restore-blob-bytes": "e366310261f149e9", "search-and-filters/restore-blob-hash": "093bca6ebc0325b2", "search-and-filters/restore-unread-entry": "97c850728a50420f", @@ -4494,11 +4545,15 @@ "stores/drop-bare-hash": "426fe552892826f4", "stores/drop-order": "094eca8d66e12bc4", "stores/drop-concurrent": "536fa8e5ff0abc50", - "stores/integrity-result": "6da0ab3e6a75c679", + "stores/integrity-result": "d236b4fab4c98be4", "stores/integrity-stamps": "88a437275a8e8020", "stores/integrity-rehashes-disk": "9fe515ed092074e7", "stores/integrity-asks-object-store": "be2e12a7fdf25040", - "stores/integrity-strikes": "316fbdbe100cd170", + "stores/integrity-strikes": "44619b680cfca514", + "stores/integrity-unreachable-keeps": "3697ff9da343c313", + "stores/integrity-unreachable-reported": "25c191c014994bc9", + "stores/integrity-unreachable-others": "3999eb5d673b89fa", + "stores/unreachable-read": "09f89783451be40e", "stores/integrity-strike-restored": "9d8bce82fba1f34a", "stores/integrity-order": "d16011c5fb1ac06a", "stores/integrity-count-bound": "4147ccf493b3e4c6", @@ -4636,6 +4691,7 @@ "types/delete-permission": "d046f81f4dddbb0f", "types/orphan-write": "a99804511e3c78c1", "types/orphan-kept-writable": "c0a5b70d7a54dd70", + "types/orphan-read": "2c89c20a37bee115", "types/orphan-reregistered": "32f0535f7fa3b225", "types/delete-race": "aedfc5419e78673e", "types/version-policy-values": "6f008a1555b41f9e", diff --git a/src/generated/error-codes.md b/src/generated/error-codes.md index 61ab2cb0..423c4bd6 100644 --- a/src/generated/error-codes.md +++ b/src/generated/error-codes.md @@ -38,7 +38,7 @@ | `sign_in_not_found` | 404 | No live sign-in of the owner has this id: it never existed, has ended or has expired. | | `type_not_found` | 404 | No registered type has this identifier. | | `webhook_not_found` | 404 | No webhook subscription this credential registered has this id. | -| `request_timeout` | 408 | An inbound webhook delivery did not finish arriving before its deadline. | +| `request_timeout` | 408 | A body did not arrive in time: an inbound webhook delivery did not finish before its deadline, or a restore or a blob upload received no bytes for the upload idle limit. | | `ancestor_unavailable` | 409 | The write names a version that has no snapshot the caller may merge against. | | `conflict` | 409 | The request collides with the current state in a way that no other code names. | | `connector_held` | 409 | Another process holds the connector's registration until `details.expires_at`. | @@ -66,7 +66,7 @@ | `idempotency_result_not_retained` | 422 | An `Idempotency-Key` repeats a request whose first answer was too large to keep. `details.original_status` is the status it carried. | | `rate_limited` | 429 | The credential, or an inbound endpoint, is past its request cap for the current window. | | `internal_error` | 500 | A fault that nothing else names a refusal for. The answer says nothing of what failed. | -| `blob_store_unavailable` | 503 | A blob store failed, or did not answer in time, while Marfa checked a blob's copy. Nothing was written. Retry later. | +| `blob_store_unavailable` | 503 | A blob store failed, or did not answer in time, while Marfa checked or read a blob's copy. Nothing was written. Retry later. | | `export_unrestorable` | 503 | Writes changed the row an archive export refused, or its type, between two reads. No archive bytes were sent. Retry the export. | | `inbound_unavailable` | 503 | An inbound endpoint cannot take a delivery now, because its connector's backlog is full or the instance holds as many bodies in flight as it allows. Retry later. | | `stream_capacity_exhausted` | 503 | The instance is serving as many live event streams as it allows. `details.reason` is `viewer_cap`. | diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index 2451bd8c..6b9ec6e1 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.16"} +{"version":"v0.0.17"} diff --git a/src/generated/server-image.md b/src/generated/server-image.md index 89c24cfd..3f5c0728 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.16 +docker pull ghcr.io/withmarfa/marfa-server:0.0.17 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.16 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.17 ``` diff --git a/src/generated/settings.md b/src/generated/settings.md index 8271f090..c38ba236 100644 --- a/src/generated/settings.md +++ b/src/generated/settings.md @@ -12,6 +12,7 @@ | `SQLITE_BUSY_BUDGET_MS` | `5000` ms | Whole number of milliseconds, 0 to 2147483647 | How long a write that meets a locked database is retried before the server answers `503 write_contention`. At 0 the first refusal is answered. | | `BLOB_PATH` | `./data/blobs` | Any text | The folder of the disk store, where every upload lands. | | `MARFA_DISK_RESERVE_BYTES` | `134217728` bytes | Whole number of bytes, 0 or more | The free space on the disk store's volume that uploads, restores, and archive exports must leave. An export's temporary files are included. A request that would take the volume below it is refused `507 insufficient_storage`. At 0 nothing is held back. | +| `MARFA_UPLOAD_IDLE_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a restore or a blob upload may receive no bytes before the server ends it `408 request_timeout`. An upload has no other time limit while bytes keep arriving. | | `MARFA_MAX_REQUEST_BYTES` | `1048576` bytes | Whole number of bytes, 1 or more | The largest ordinary JSON request body. A larger body is answered `413 request_too_large`. Bulk writes, connector state, client registration, blob uploads and inbound webhook deliveries have separate limits. | | `MARFA_MAX_BULK_REQUEST_BYTES` | `16777216` bytes | Whole number of bytes, 1 or more | The largest body the bulk operations accept. | | `S3_BUCKET` | None | Any text | The bucket of an S3-compatible object store. The store is attached when this is set. | From 8de0e00073d55e32e5db224292d93e0e96d1a75d Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Tue, 20 Oct 2026 03:08:29 +0100 Subject: [PATCH 2/5] docs(changelog): add the v0.0.17 release --- content/docs/(reference)/changelog.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index 420829af..c1e982bf 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -7,6 +7,25 @@ Each release of Marfa ships the server and its container image, the CLI for macO **Until the first public release, Marfa does not upgrade a database in place.** A build refuses a database whose schema differs from its own or that holds retired tables or settings, and leaves the file unchanged. If the build refuses your database, [Upgrade](/guides/upgrade) covers exporting with the old build and restoring into fresh storage. A version change alone does not cause a refusal. +## v0.0.17 + +Released October 20, 2026. [v0.0.17 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.17) + +For running an instance: + +- **A blob folder that has gone is no longer read as an empty one.** When the disk store's folder or its `.marfa-store` marker is missing, as when a drive didn't mount, `GET /health` answers `blob_storage` as `down` with status `503`. The `blob-integrity` job strikes none of that store's copies, counts the store in `unreachable` in its result, and still checks the stores it can reach. A blob read answers `503 blob_store_unavailable` rather than `404 blob_not_found` when no store that holds a copy can answer, and a restore that can't reach the disk to place a blob answers the same before it writes anything, where it answered `500`. Replication doesn't end its run on such a store, and the text-extraction sweep doesn't count the attempt. [Deploy](/guides/deploy#keep-it-running) covers the health check. +- **The bin purge keeps going through a backlog.** `trash-purge` removes a limited number of items in a run. While items past the retention remain after a run that removed some, the job runs again at once instead of waiting for its next interval, so an instance that bins more items than a run removes no longer falls behind. [Housekeeping](/guides/housekeeping#empty-the-bin) covers the job. +- **A restore keeps an item or an edge as the archive holds it.** An item whose type the instance no longer registers, such as after a forced delete, or that lacks a field its type later required, is restored with the properties and `schema_version` the archive holds. So is an edge whose edge type was force-deleted, and an edge with an end that is an item of a force-deleted type. An export no longer refuses these with `409 export_rows_unrestorable`, so an instance that holds them can be exported again. An item of a `core` or `system` type that the build ships is still held to its shape. [Back up and restore](/guides/back-up-and-restore#restore-an-archive) covers it. +- **A restore refuses an archive that lacks one of its four files.** An archive without `manifest.json`, `items.ndjson`, `edges.ndjson` or `types.ndjson` is refused `400 validation_error`, naming each missing entry in the message and in `details.entries`. Before, it answered `200` with counts of zero. +- **A restore or an upload is no longer cut off while its bytes arrive.** An admitted upload has no total time limit. `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60000 milliseconds unless set, ends one that receives no bytes for that long with `408 request_timeout`, and a restore then writes nothing and an upload stores no blob. The owner's recent sign-in is judged when a restore starts, so a restore that starts within five minutes of the sign-in isn't refused for finishing later. A session that signed in earlier is refused `403 forbidden` without waiting for the body to finish arriving, and one that ends during the upload is answered `401 unauthorized`. [Settings](/guides/settings#every-environment-setting) lists the setting. +- **`marfa export --output` keeps the last whole archive.** When the new archive lacks blobs, the CLI doesn't replace the file at the path. It keeps the archive beside it as `.incomplete`, replacing only an earlier file of that name, and exits `1` with `blobs_missing`. The message says whether the path still holds an earlier file. If the CLI can't write that name, it keeps the archive in a hidden file beside the path and names the file. [Back up and restore](/guides/back-up-and-restore#when-an-archive-lacks-a-files-bytes) covers it. +- **Claiming in a browser signs the owner in, and setup is six steps.** A claim that carries the instance's own `Origin` leaves the browser signed in, as a sign-in does, and that counts as a sign-in within the last five minutes, so adding a passkey needs no second prompt. A claim that sends no `Origin`, such as a script's, and a claim that is refused set no session. If the sign-in that follows a claim fails, the owner stays claimed and signs in at the page that follows. Setup runs as six steps: the setup code, the owner's details, a passkey, a name for the instance, connecting, and backups. [Quickstart](/quickstart#claim-the-instance) covers claiming. +- **The sign-in page says how long a lockout lasts.** When password sign-in or a device code lookup is refused for too many attempts, the page states the wait in whole minutes, rounded up, and the sign-in page says that a passkey still signs in. The wait runs to the time another attempt is allowed, the latest end among the limits that are spent. The sign-in page also links to a Forgot password page at `/auth/forgot-password`, which gives the command that recovers the owner on the instance's machine. [Troubleshooting](/guides/troubleshooting) covers it. + +For apps and devices: + +- **An export ends with an error when its credential ends.** If the key or signed-in app that started `GET /export` is revoked, expires or is disconnected while the response is open, the response ends with an error instead of ending cleanly, in both formats and while an archive sends a blob. The same happens if the credential is narrowed so that it no longer reads something the export has still to send. A narrowing that takes away nothing the export has still to send changes nothing. A key revoked before the response begins gets `401 unauthorized`. [Back up and restore](/guides/back-up-and-restore#what-an-export-checks) covers it. + ## v0.0.16 Released October 19, 2026. [v0.0.16 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.16) From b44a8628dab5039cf52e6a0fc6bfbef20a2be253 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Tue, 20 Oct 2026 03:08:29 +0100 Subject: [PATCH 3/5] docs(guides): align the guides with marfa v0.0.17 --- content/docs/(overview)/quickstart.mdx | 2 +- content/docs/guides/back-up-and-restore.mdx | 20 ++++++++++++++------ content/docs/guides/deploy.mdx | 2 +- content/docs/guides/handle-files.mdx | 2 +- content/docs/guides/housekeeping.mdx | 2 +- content/docs/guides/troubleshooting.mdx | 9 ++++++++- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/content/docs/(overview)/quickstart.mdx b/content/docs/(overview)/quickstart.mdx index b260a194..b143802e 100644 --- a/content/docs/(overview)/quickstart.mdx +++ b/content/docs/(overview)/quickstart.mdx @@ -74,7 +74,7 @@ The CLI asks for a password for the owner, then for the setup code the server pr -Open `http://localhost:8600/setup`, enter the setup code the server printed, and choose the owner's email and password. +Open `http://localhost:8600/setup`, enter the setup code the server printed, and choose the owner's email and password. **Claiming signs you in**, and setup goes on from there to add a passkey, name the instance, connect something and read how it's backed up. diff --git a/content/docs/guides/back-up-and-restore.mdx b/content/docs/guides/back-up-and-restore.mdx index 18c4ff5e..29b3beb7 100644 --- a/content/docs/guides/back-up-and-restore.mdx +++ b/content/docs/guides/back-up-and-restore.mdx @@ -44,6 +44,8 @@ S3_PREFIX=blobs litestream restore -config deploy/litestream.yml -o restored.db The [deployment guide](https://github.com/withmarfa/marfa/blob/main/deploy/README.md#restoring-by-hand) has the same command for a point in time. +**A copy of the volume that is older than the bucket needs a prefix of its own.** Started under the bucket's current `S3_PREFIX`, it becomes the bucket's newest copy of the database. The deployment guide's [restore of a backup older than the bucket](https://github.com/withmarfa/marfa/blob/main/deploy/README.md#restore-a-backup-that-is-older-than-the-bucket) gives the steps. + ## Export an archive **An archive holds what its caller can read.** A live owner browser session can export without a key or recent reauthentication. For automated exports with the CLI, make a key that reads everything and use it only for exports. The steps below use that key. @@ -95,17 +97,17 @@ types.ndjson blobs/sha256:a0feecd7233d9f5f3ec784c6bdb36587c651e2ec60437c27b4bf135de39149d7 ``` -If the server fails partway through an export, it ends the connection early, and the file doesn't unpack. **`marfa export --output` writes to a file beside the path and renames it only once the stream has ended whole.** A stream cut short removes that file, leaves any earlier file at the path as it was, and ends with exit code 3, so try again. +If the server fails partway through an export, it ends the connection early, and the file doesn't unpack. **`marfa export --output` writes to a hidden file beside the path and renames it over the path only once the stream has ended whole and the archive lacks no blobs.** A stream cut short removes that file, leaves any earlier file at the path as it was, and ends with exit code 3, so try again. ### Check what the archive lacks -The manifest names each file's bytes the archive couldn't carry, under `missing_blobs`. Read it from the archive: +The manifest names each file's bytes the archive couldn't carry, under `missing_blobs`. When `marfa export` exits with code 1 and `blobs_missing`, the archive is at `marfa-archive.tar.gz.incomplete`, not at `marfa-archive.tar.gz`. Read the manifest from it: ```bash -tar -xzOf marfa-archive.tar.gz manifest.json +tar -xzOf marfa-archive.tar.gz.incomplete manifest.json ``` The manifest's other fields are left out here: @@ -132,7 +134,7 @@ An empty `missing_blobs` means the archive holds every blob its items name. The An export never claims a blob it doesn't hold. When no store holds a whole copy of a blob, such as after a disk lost the file, the export **leaves the blob out of the archive** and names it in `missing_blobs`, with its `mime_type` and `size_bytes`. The rest of the instance is still backed up: one lost file costs the backup that file, not everything. -`marfa export` writes the archive, then **exits with code 1 and `blobs_missing`**, naming up to ten of the hashes and counting the rest. A script that reads only the exit code can't take such an archive for a whole one. The HTTP answer is still `200`, so read the manifest if you export without the CLI. +`marfa export` **exits with code 1 and `blobs_missing`**, naming up to ten of the hashes and counting the rest. **With `--output`, it doesn't replace the file at the path.** It keeps the archive beside it as `.incomplete`, replacing only an earlier file of that name, so the last whole backup stays where a script reads it. If it can't write that name, it keeps the archive in a hidden file beside the path and names the file in its message. A script that reads only the exit code can't take such an archive for a whole one. The HTTP answer is still `200`, so read the manifest if you export without the CLI. A file item can name bytes the instance has no record of, as after a restore that lacked them. The export names such a blob too, with `null` for both `mime_type` and `size_bytes`, because nothing knows them. **Every later backup keeps naming a file's blob it lacks until the bytes are uploaded again**, with `marfa blobs upload` or [Upload a blob](/api-reference/blobs/uploadBlob). Only a file item's `blob_ref` counts: a hash that appears in a note's text is not named. @@ -142,6 +144,8 @@ A store that fails, or doesn't answer within 15 seconds, is not a lost file. The **An export isn't taken at one instant**: an item created while it runs may not be in it. Stop your apps first if the archive must hold every write. +**An export ends when its credential does.** If the key or signed-in app that started it is revoked, expires or is disconnected, or is narrowed so that it no longer reads something the export has still to send, the server ends the response with an error, in both formats, and the file doesn't unpack. A narrowing that takes away nothing the export has still to send changes nothing. + **Before it sends a byte, Marfa checks the whole archive the way a restore into an empty instance would.** An archive that Marfa sends restores into an empty instance of the same build without skipping an item or an edge, even when writes landed between its pages. If a row fails the check, Marfa reads the selection once more. If the second read fails too, Marfa answers one of two ways, and neither sends an archive: - **`409 export_rows_unrestorable`:** both reads refused the same row, and the row didn't change between them. The cause is in your data, such as a row that a type's replacement schema no longer accepts. `details.row` names the row by `kind`, `id`, `type` and `field`, with the `code` a restore refuses it with or the reason it skips it. Correct the row or its type, then export again. @@ -153,7 +157,7 @@ A [source filter](/guides/settings#what-the-enforcement-levers-do) leaves items ## Restore an archive -**Only the owner can restore**, through the private socket or signed in to the instance within the last five minutes. A key is refused `403 forbidden`, whatever it holds. The target can be a new instance or the one the archive came from: +**Only the owner can restore**, through the private socket or signed in to the instance within the five minutes before the restore starts. A key is refused `403 forbidden`, whatever it holds. The target can be a new instance or the one the archive came from: ```bash marfa --socket /srv/marfa/data/control/marfa.sock restore marfa-archive.tar.gz @@ -183,17 +187,21 @@ A restored `system.connection` item for an app has `status: revoked`. It records **An edge to an item in the bin is restored**, as it was exported. A restore skips an edge only for one of four reasons, which `edges_skipped_reasons` counts: `already_present`, `endpoint_missing` (the instance holds no item under an endpoint's ID, in the bin or out of it), `malformed`, or the error code that `POST /edges` would answer for the edge, such as `edge_cycle`. An archive's types restore in any order, including types that name each other in `compatible_with`. +**A restore takes an item as the archive holds it** when its type isn't registered, as after a forced delete, or when it lacks a field its type later required, and so for an edge whose edge type was force-deleted or whose end is an item of such a type. An item of a `core` or `system` type that the build ships is still held to its shape. + An item the instance already holds is counted in `duplicates` and left as it is, so replaying an archive never overwrites work done since. Restoring the same archive again into the same instance answers `"imported": 0` and `"duplicates": 5`. `blobs_missing` lists the hashes the archive names but doesn't carry bytes for, that no store of the instance holds either. The restore still writes the items, and **writes no record for those blobs**, so a file that names one is a file with no bytes until you upload them again with `marfa blobs upload`. `marfa restore` prints the answer and exits with code 1 and `blobs_missing` when the list isn't empty, naming up to ten of the hashes. The same check covers an archive whose blob entry is missing or doesn't hash to its name. A restore writes in one transaction. If it's refused, fails or is interrupted, it leaves nothing behind, so you can run it again. While it runs, other writes wait for it, so let it answer before you start your apps. +**A restore has no time limit while its bytes arrive.** If none arrives for `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60 seconds unless you set it, the server answers `408 request_timeout` and writes nothing. A browser session that ends during the upload is answered `401 unauthorized`, also with nothing written. + **An archive never holds keys, webhooks, the configuration or the audit log**: make keys, register webhooks and set the configuration again on the new instance. Before Marfa's first release, only the build that wrote an archive is sure to read it, so **an archive isn't a long-term backup**. A restore refuses the whole archive, and writes nothing, for one of these reasons: -- **`400 validation_error`:** the file isn't a valid archive, is at another format version, repeats an entry, has an invalid row, holds a file over 64 MiB, or holds text that isn't well-formed UTF-16. For that text, the answer names the entry or the row and the JSON pointer of the text. +- **`400 validation_error`:** the file isn't a valid archive, lacks `manifest.json`, `items.ndjson`, `edges.ndjson` or `types.ndjson` (`details.entries` names each), is at another format version, repeats an entry, has an invalid row, holds a file over 64 MiB, or holds text that isn't well-formed UTF-16. For that text, the answer names the entry or the row and the JSON pointer of the text. - **`400 invalid_schema`:** an archived type is invalid or carries a key the schema doesn't define. - **`400 invalid_properties`:** an item sets a property its type doesn't declare, and strict mode names that type. - **`409 conflict`:** the archive redefines a type the instance registers differently, carries a core edge type, or carries a snapshot ID the instance already holds. diff --git a/content/docs/guides/deploy.mdx b/content/docs/guides/deploy.mdx index e449afb0..d530c184 100644 --- a/content/docs/guides/deploy.mdx +++ b/content/docs/guides/deploy.mdx @@ -172,7 +172,7 @@ Claim it with `marfa --socket setup open`, as the account that runs the s ## Keep it running -`GET /health` answers without a credential. It answers `503` when the database, a write, the disk or blob storage is down, and `200` otherwise, a degraded component included. **Point your supervisor or monitor at it**; the container checks it every 30 seconds. +`GET /health` answers without a credential. It answers `503` when the database, a write, the disk or blob storage is down, and `200` otherwise, a degraded component included. **Blob storage is down when the blob folder or its `.marfa-store` marker is gone**, as when a drive didn't mount, and the instance strikes none of that store's copies while it is. **Point your supervisor or monitor at it**; the container checks it every 30 seconds. `SIGTERM` or `SIGINT` stops the server within eight seconds, so **give it ten before a runtime kills the process**. diff --git a/content/docs/guides/handle-files.mdx b/content/docs/guides/handle-files.mdx index f7170940..1bc285a9 100644 --- a/content/docs/guides/handle-files.mdx +++ b/content/docs/guides/handle-files.mdx @@ -48,7 +48,7 @@ Marfa answers with the blob's `hash`, the media type it will serve the bytes und } ``` -The same bytes always give the same hash, so **uploading them again stores nothing new** and answers the same hash. The Swift SDK queues the upload like any other write and sends it when the app drains its queue; the hash is known at once. +The same bytes always give the same hash, so **uploading them again stores nothing new** and answers the same hash. **An upload has no size cap and no total time limit.** Marfa ends one with `408 request_timeout`, storing no blob, only when no bytes arrive for `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60 seconds unless the instance sets it. The Swift SDK queues the upload like any other write and sends it when the app drains its queue; the hash is known at once. ## Point an item at the bytes diff --git a/content/docs/guides/housekeeping.mdx b/content/docs/guides/housekeeping.mdx index 2e70e9a9..47ad0f91 100644 --- a/content/docs/guides/housekeeping.mdx +++ b/content/docs/guides/housekeeping.mdx @@ -93,7 +93,7 @@ The server refuses `409 housekeeping_job_running` while a run of the same housek ## Empty the bin -**An item stays in the bin until it is purged, and a purge is permanent.** Left alone, the `trash-purge` job purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. +**An item stays in the bin until it is purged, and a purge is permanent.** Left alone, the `trash-purge` job purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items, and while items past the retention remain, the job runs again at once rather than waiting for its interval. To empty the bin now, purge every item whose state is `trashed`. A key needs the `items.purge` permission, and **the private socket can't do it**, because it carries no key: diff --git a/content/docs/guides/troubleshooting.mdx b/content/docs/guides/troubleshooting.mdx index fdeec244..2b5e6022 100644 --- a/content/docs/guides/troubleshooting.mdx +++ b/content/docs/guides/troubleshooting.mdx @@ -123,7 +123,7 @@ Sign in again at `/auth/sign-in?prompt=login`, then repeat the operation. Withou -The sign-in page, or a `429` with `Retry-After`, says the owner's password was tried too often from one address. Wait, or sign in with a passkey. [Manage keys](/guides/manage-keys#recover-the-owners-password) has the limit and how to lift the lock, and [Deploy](/guides/deploy#the-address) says why, behind a proxy, every client can share one address. +The sign-in page, or a `429` with `Retry-After`, says the owner's password was tried too often. The page says how many minutes to wait, rounded up, and that a passkey signs in during the lock. A device code page that refuses for too many attempts names its wait the same way. [Manage keys](/guides/manage-keys#recover-the-owners-password) has the limit and how to lift the lock, and [Deploy](/guides/deploy#the-address) says why, behind a proxy, every client can share one address. @@ -142,6 +142,13 @@ The credential made more requests in the current window than the instance allows Wait that long, then send the request again. The limits are set by the `RATE_LIMIT_*` settings in [Settings](/guides/settings). + + + +The server received no bytes of a restore or a blob upload for `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60 seconds unless the instance sets it, so it ended the request. A restore writes nothing and an upload stores no blob. A slow upload isn't ended while bytes keep arriving. + +Send the request again over a connection that stays up. [Settings](/guides/settings#every-environment-setting) lists the setting. + From 3e797057602b1ac482ccdf3b17c30cf59100d761 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Tue, 20 Oct 2026 03:08:58 +0100 Subject: [PATCH 4/5] docs(guides): say what a blob download answers when no store can --- content/docs/guides/handle-files.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/guides/handle-files.mdx b/content/docs/guides/handle-files.mdx index 1bc285a9..2bef456e 100644 --- a/content/docs/guides/handle-files.mdx +++ b/content/docs/guides/handle-files.mdx @@ -127,7 +127,7 @@ let local = try await copy.blobs.get(hash) In the Swift SDK, `get` answers a file on the device, and fetches the bytes first if the working copy doesn't hold them yet. -Marfa sends the bytes as an attachment named after the hash, never as a page a browser would show, so **take the file's name from the item's `title`**. A `Range` header such as `bytes=0-1023` gets part of a blob. A blob you may not read answers `404 blob_not_found`, exactly as a hash that doesn't exist does. +Marfa sends the bytes as an attachment named after the hash, never as a page a browser would show, so **take the file's name from the item's `title`**. A `Range` header such as `bytes=0-1023` gets part of a blob. A blob you may not read answers `404 blob_not_found`, exactly as a hash that doesn't exist does. When no store that holds the blob answers, Marfa answers `503 blob_store_unavailable` instead, because it can't tell whether the bytes are gone: try again later. ## Hand out a time-limited link From c5a24c6ab5db70c541d73a3ea1763d8597cd1d3d Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Tue, 20 Oct 2026 03:18:33 +0100 Subject: [PATCH 5/5] docs(release): correct v0.0.17 claims against the tag --- content/docs/(reference)/changelog.mdx | 10 +++++----- content/docs/guides/back-up-and-restore.mdx | 6 +++--- content/docs/guides/deploy.mdx | 2 +- content/docs/guides/handle-files.mdx | 2 +- content/docs/guides/housekeeping.mdx | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index c1e982bf..01a8b0c2 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -13,18 +13,18 @@ Released October 20, 2026. [v0.0.17 on GitHub](https://github.com/withmarfa/marf For running an instance: -- **A blob folder that has gone is no longer read as an empty one.** When the disk store's folder or its `.marfa-store` marker is missing, as when a drive didn't mount, `GET /health` answers `blob_storage` as `down` with status `503`. The `blob-integrity` job strikes none of that store's copies, counts the store in `unreachable` in its result, and still checks the stores it can reach. A blob read answers `503 blob_store_unavailable` rather than `404 blob_not_found` when no store that holds a copy can answer, and a restore that can't reach the disk to place a blob answers the same before it writes anything, where it answered `500`. Replication doesn't end its run on such a store, and the text-extraction sweep doesn't count the attempt. [Deploy](/guides/deploy#keep-it-running) covers the health check. -- **The bin purge keeps going through a backlog.** `trash-purge` removes a limited number of items in a run. While items past the retention remain after a run that removed some, the job runs again at once instead of waiting for its next interval, so an instance that bins more items than a run removes no longer falls behind. [Housekeeping](/guides/housekeeping#empty-the-bin) covers the job. -- **A restore keeps an item or an edge as the archive holds it.** An item whose type the instance no longer registers, such as after a forced delete, or that lacks a field its type later required, is restored with the properties and `schema_version` the archive holds. So is an edge whose edge type was force-deleted, and an edge with an end that is an item of a force-deleted type. An export no longer refuses these with `409 export_rows_unrestorable`, so an instance that holds them can be exported again. An item of a `core` or `system` type that the build ships is still held to its shape. [Back up and restore](/guides/back-up-and-restore#restore-an-archive) covers it. +- **A blob folder that has gone is no longer read as an empty one.** When the disk store's folder or its `.marfa-store` marker is missing, as when its drive is unmounted while the server runs, `GET /health` answers `blob_storage` as `down` with status `503`. The `blob-integrity` job strikes none of that store's copies, counts the store in `unreachable` in its result, and still checks the stores it can reach. A blob read answers `503 blob_store_unavailable` rather than `404 blob_not_found` when a store that holds a copy can't answer and no other store returns the bytes. A restore that finds the disk store's marker missing, or naming another store, when it places a blob answers the same and writes nothing. Replication doesn't end its run on such a store, and the `enrichment-sweep` job doesn't count the attempt against the file. [Deploy](/guides/deploy#keep-it-running) covers the health check. +- **The bin purge keeps going through a backlog.** `trash-purge` removes a limited number of items in a run. While items past the retention remain after a run that removed some, the job runs again at once instead of waiting for its next interval, so an instance that bins more items in an interval than a run removes no longer falls behind. [Housekeeping](/guides/housekeeping#empty-the-bin) covers the job. +- **A restore keeps an item or an edge as the archive holds it.** An item whose type the instance no longer registers, such as after a forced delete, or that lacks a field its type later required, is restored with the properties and `schema_version` the archive holds. A restore also keeps an edge whose edge type was force-deleted, and no longer judges an end that is an item of a force-deleted type against the edge type's constraints. Such an edge is still skipped for the other reasons, such as a missing end. An export no longer refuses these with `409 export_rows_unrestorable`, so an instance that holds them can be exported again. An item of a `core` or `system` type that the build ships is still held to its shape. [Back up and restore](/guides/back-up-and-restore#restore-an-archive) covers it. - **A restore refuses an archive that lacks one of its four files.** An archive without `manifest.json`, `items.ndjson`, `edges.ndjson` or `types.ndjson` is refused `400 validation_error`, naming each missing entry in the message and in `details.entries`. Before, it answered `200` with counts of zero. -- **A restore or an upload is no longer cut off while its bytes arrive.** An admitted upload has no total time limit. `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60000 milliseconds unless set, ends one that receives no bytes for that long with `408 request_timeout`, and a restore then writes nothing and an upload stores no blob. The owner's recent sign-in is judged when a restore starts, so a restore that starts within five minutes of the sign-in isn't refused for finishing later. A session that signed in earlier is refused `403 forbidden` without waiting for the body to finish arriving, and one that ends during the upload is answered `401 unauthorized`. [Settings](/guides/settings#every-environment-setting) lists the setting. +- **A restore or an upload is no longer cut off while its bytes arrive.** An admitted upload has no total time limit. `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60000 milliseconds unless set, ends one that receives no bytes for that long with `408 request_timeout`, and a restore then writes nothing and an upload stores no blob. The owner's recent sign-in is judged when a restore starts, so a restore that starts within five minutes of the sign-in isn't refused for finishing later. A session that signed in more than five minutes before the restore starts is refused `403 forbidden` without waiting for the body to finish arriving, and one that ends during the upload is answered `401 unauthorized`. [Settings](/guides/settings#every-environment-setting) lists the setting. - **`marfa export --output` keeps the last whole archive.** When the new archive lacks blobs, the CLI doesn't replace the file at the path. It keeps the archive beside it as `.incomplete`, replacing only an earlier file of that name, and exits `1` with `blobs_missing`. The message says whether the path still holds an earlier file. If the CLI can't write that name, it keeps the archive in a hidden file beside the path and names the file. [Back up and restore](/guides/back-up-and-restore#when-an-archive-lacks-a-files-bytes) covers it. - **Claiming in a browser signs the owner in, and setup is six steps.** A claim that carries the instance's own `Origin` leaves the browser signed in, as a sign-in does, and that counts as a sign-in within the last five minutes, so adding a passkey needs no second prompt. A claim that sends no `Origin`, such as a script's, and a claim that is refused set no session. If the sign-in that follows a claim fails, the owner stays claimed and signs in at the page that follows. Setup runs as six steps: the setup code, the owner's details, a passkey, a name for the instance, connecting, and backups. [Quickstart](/quickstart#claim-the-instance) covers claiming. - **The sign-in page says how long a lockout lasts.** When password sign-in or a device code lookup is refused for too many attempts, the page states the wait in whole minutes, rounded up, and the sign-in page says that a passkey still signs in. The wait runs to the time another attempt is allowed, the latest end among the limits that are spent. The sign-in page also links to a Forgot password page at `/auth/forgot-password`, which gives the command that recovers the owner on the instance's machine. [Troubleshooting](/guides/troubleshooting) covers it. For apps and devices: -- **An export ends with an error when its credential ends.** If the key or signed-in app that started `GET /export` is revoked, expires or is disconnected while the response is open, the response ends with an error instead of ending cleanly, in both formats and while an archive sends a blob. The same happens if the credential is narrowed so that it no longer reads something the export has still to send. A narrowing that takes away nothing the export has still to send changes nothing. A key revoked before the response begins gets `401 unauthorized`. [Back up and restore](/guides/back-up-and-restore#what-an-export-checks) covers it. +- **An export ends with an error when its credential ends.** If the key or signed-in app that started `GET /export` is revoked, expires or is disconnected while the response is open, the response ends with an error instead of ending cleanly, in both formats and while an archive sends a blob. The same happens if the credential is narrowed so that it no longer reads something the export has still to send. A narrowing that takes away nothing the export has still to send changes nothing. An archive export whose key is revoked while it reads the selection, before the response begins, gets `401 unauthorized`. [Back up and restore](/guides/back-up-and-restore#what-an-export-checks) covers it. ## v0.0.16 diff --git a/content/docs/guides/back-up-and-restore.mdx b/content/docs/guides/back-up-and-restore.mdx index 29b3beb7..4b47e324 100644 --- a/content/docs/guides/back-up-and-restore.mdx +++ b/content/docs/guides/back-up-and-restore.mdx @@ -144,11 +144,11 @@ A store that fails, or doesn't answer within 15 seconds, is not a lost file. The **An export isn't taken at one instant**: an item created while it runs may not be in it. Stop your apps first if the archive must hold every write. -**An export ends when its credential does.** If the key or signed-in app that started it is revoked, expires or is disconnected, or is narrowed so that it no longer reads something the export has still to send, the server ends the response with an error, in both formats, and the file doesn't unpack. A narrowing that takes away nothing the export has still to send changes nothing. +**An export ends when its credential does.** If the key or signed-in app that started it is revoked, expires or is disconnected, or is narrowed so that it no longer reads something the export has still to send, the server ends the response with an error, in both formats, and an archive cut short this way doesn't unpack. A narrowing that takes away nothing the export has still to send changes nothing. **Before it sends a byte, Marfa checks the whole archive the way a restore into an empty instance would.** An archive that Marfa sends restores into an empty instance of the same build without skipping an item or an edge, even when writes landed between its pages. If a row fails the check, Marfa reads the selection once more. If the second read fails too, Marfa answers one of two ways, and neither sends an archive: -- **`409 export_rows_unrestorable`:** both reads refused the same row, and the row didn't change between them. The cause is in your data, such as a row that a type's replacement schema no longer accepts. `details.row` names the row by `kind`, `id`, `type` and `field`, with the `code` a restore refuses it with or the reason it skips it. Correct the row or its type, then export again. +- **`409 export_rows_unrestorable`:** both reads refused the same row, and the row didn't change between them. The cause is in your data, such as a type replaced to become an event while one of its rows holds a recurrence rule that can't be read. `details.row` names the row by `kind`, `id`, `type` and `field`, with the `code` a restore refuses it with or the reason it skips it. Correct the row or its type, then export again. - **`503 export_unrestorable`:** writes changed the refused row or its type between the two reads, or the second read refused a different row, so the data may be fine once the writes stop. Export again, or stop your apps first. The server prepares the whole archive before it sends the first byte, in a folder inside the blob folder, so leave room on that disk for about as much as the archive's text takes uncompressed. An export that would take the volume below `MARFA_DISK_RESERVE_BYTES` is refused `507 insufficient_storage`, as an upload or a restore is. @@ -187,7 +187,7 @@ A restored `system.connection` item for an app has `status: revoked`. It records **An edge to an item in the bin is restored**, as it was exported. A restore skips an edge only for one of four reasons, which `edges_skipped_reasons` counts: `already_present`, `endpoint_missing` (the instance holds no item under an endpoint's ID, in the bin or out of it), `malformed`, or the error code that `POST /edges` would answer for the edge, such as `edge_cycle`. An archive's types restore in any order, including types that name each other in `compatible_with`. -**A restore takes an item as the archive holds it** when its type isn't registered, as after a forced delete, or when it lacks a field its type later required, and so for an edge whose edge type was force-deleted or whose end is an item of such a type. An item of a `core` or `system` type that the build ships is still held to its shape. +**A restore takes an item as the archive holds it** when its type isn't registered, as after a forced delete, or when it lacks a field its type later required or holds a value the type's field no longer accepts. An item of a `core` or `system` type that the build ships is still held to its shape. A restore also keeps an edge whose edge type isn't registered, and doesn't judge an end whose type isn't registered against the edge type's constraints; the other reasons to skip an edge still apply. An item the instance already holds is counted in `duplicates` and left as it is, so replaying an archive never overwrites work done since. Restoring the same archive again into the same instance answers `"imported": 0` and `"duplicates": 5`. diff --git a/content/docs/guides/deploy.mdx b/content/docs/guides/deploy.mdx index d530c184..34ab2f72 100644 --- a/content/docs/guides/deploy.mdx +++ b/content/docs/guides/deploy.mdx @@ -172,7 +172,7 @@ Claim it with `marfa --socket setup open`, as the account that runs the s ## Keep it running -`GET /health` answers without a credential. It answers `503` when the database, a write, the disk or blob storage is down, and `200` otherwise, a degraded component included. **Blob storage is down when the blob folder or its `.marfa-store` marker is gone**, as when a drive didn't mount, and the instance strikes none of that store's copies while it is. **Point your supervisor or monitor at it**; the container checks it every 30 seconds. +`GET /health` answers without a credential. It answers `503` when the database, a write, the disk or blob storage is down, and `200` otherwise, a degraded component included. **Blob storage is down when the blob folder or its `.marfa-store` marker is gone**, as when its drive is unmounted while the server runs, and the instance strikes none of that store's copies while it is. **Point your supervisor or monitor at it**; the container checks it every 30 seconds. `SIGTERM` or `SIGINT` stops the server within eight seconds, so **give it ten before a runtime kills the process**. diff --git a/content/docs/guides/handle-files.mdx b/content/docs/guides/handle-files.mdx index 2bef456e..99b627cb 100644 --- a/content/docs/guides/handle-files.mdx +++ b/content/docs/guides/handle-files.mdx @@ -127,7 +127,7 @@ let local = try await copy.blobs.get(hash) In the Swift SDK, `get` answers a file on the device, and fetches the bytes first if the working copy doesn't hold them yet. -Marfa sends the bytes as an attachment named after the hash, never as a page a browser would show, so **take the file's name from the item's `title`**. A `Range` header such as `bytes=0-1023` gets part of a blob. A blob you may not read answers `404 blob_not_found`, exactly as a hash that doesn't exist does. When no store that holds the blob answers, Marfa answers `503 blob_store_unavailable` instead, because it can't tell whether the bytes are gone: try again later. +Marfa sends the bytes as an attachment named after the hash, never as a page a browser would show, so **take the file's name from the item's `title`**. A `Range` header such as `bytes=0-1023` gets part of a blob. A blob you may not read answers `404 blob_not_found`, exactly as a hash that doesn't exist does. When a store that holds the blob can't answer and no other store returns the bytes, Marfa answers `503 blob_store_unavailable` instead, because it can't tell whether the bytes are gone: try again later. ## Hand out a time-limited link diff --git a/content/docs/guides/housekeeping.mdx b/content/docs/guides/housekeeping.mdx index 47ad0f91..65e2355a 100644 --- a/content/docs/guides/housekeeping.mdx +++ b/content/docs/guides/housekeeping.mdx @@ -93,7 +93,7 @@ The server refuses `409 housekeeping_job_running` while a run of the same housek ## Empty the bin -**An item stays in the bin until it is purged, and a purge is permanent.** Left alone, the `trash-purge` job purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items, and while items past the retention remain, the job runs again at once rather than waiting for its interval. +**An item stays in the bin until it is purged, and a purge is permanent.** Left alone, the `trash-purge` job purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items, and while items past the retention remain after a run that removed some, the job runs again at once rather than waiting for its interval. To empty the bin now, purge every item whose state is `trashed`. A key needs the `items.purge` permission, and **the private socket can't do it**, because it carries no key: