From f0e0d8072d8437bdd3955d5d389d30381fef30aa Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Thu, 22 Oct 2026 19:27:53 +0100 Subject: [PATCH 1/3] chore(release): sync the generated files with marfa v0.0.26 --- openapi.json | 1897 +++++++++++++++++++++++------ src/generated/cli-commands.json | 1 + src/generated/cli-download.md | 2 +- src/generated/contract-index.json | 41 +- src/generated/marfa-release.json | 2 +- src/generated/server-image.md | 4 +- 6 files changed, 1532 insertions(+), 415 deletions(-) diff --git a/openapi.json b/openapi.json index de591aa9..7b230927 100644 --- a/openapi.json +++ b/openapi.json @@ -292,7 +292,7 @@ "read", "write" ], - "description": "Access to the owner's avatar URL." + "description": "Access to the owner's profile picture." } }, "additionalProperties": false, @@ -7325,7 +7325,7 @@ "string", "null" ], - "description": "The owner's picture URL, or `null` when none is set." + "description": "The address of the owner's picture on this instance, or `null` when none is set. Fetch it with `GET /owner/picture`." }, "created_at": { "type": "string", @@ -7708,6 +7708,41 @@ ], "description": "An error response." }, + "NotFoundRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "not_found" + ], + "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." + }, "ExportRowsUnrestorableRefusal": { "type": "object", "properties": { @@ -46255,27 +46290,916 @@ } }, "401": { - "description": "An error in the shape RFC 6750 defines:\n- `invalid_token`: the request sends a bearer token, such as an API key. Register with no credential.", + "description": "An error in the shape RFC 6750 defines:\n- `invalid_token`: the request sends a bearer token, such as an API key. Register with no credential.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": [ + "invalid_token" + ], + "description": "The OAuth error code." + }, + "error_description": { + "type": "string", + "description": "A description of the error for a person to read." + } + }, + "required": [ + "error" + ] + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "415": { + "description": "An error with `message` and `code`, rather than Marfa's own shape:\n- `UNSUPPORTED_MEDIA_TYPE`: the request doesn't send its body as `application/json`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "UNSUPPORTED_MEDIA_TYPE" + ], + "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." + } + }, + "required": [ + "code", + "message" + ] + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "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" + } + } + } + } + } + }, + "/auth/grants": { + "get": { + "operationId": "listAppGrants", + "tags": [ + "Apps" + ], + "security": [ + { + "bearerAuth": [] + }, + { + "ownerSession": [] + } + ], + "summary": "List app grants", + "description": "Returns every active app grant in one page. Requires `grants.manage` or direct authority.", + "responses": { + "200": { + "description": "Returns every active app grant.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppGrantList" + } + } + }, + "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": "- `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" + } + } + }, + "403": { + "description": "- `forbidden`: the credential lacks `grants.manage`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenRefusal" + } + } + }, + "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" + } + } + } + } + } + }, + "/auth/grants/{id}": { + "patch": { + "operationId": "narrowAppGrant", + "tags": [ + "Apps" + ], + "security": [ + { + "ownerSession": [] + } + ], + "summary": "Narrow an app grant", + "description": "Narrows standing access. Requires a recent owner sign-in or private authority. Overbroad tokens, their refresh chains and pending codes end; covered chains keep working. The app may need to reconnect. Minted keys keep working.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the app's grant." + }, + "required": true, + "description": "The ID of the app's grant.", + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "maxItems": 1000, + "description": "The complete resulting scope set, no wider than the current grant." + }, + "expected_scopes": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000, + "description": "The complete scope set last read from the grant. A changed grant refuses the update." + } + }, + "required": [ + "scopes", + "expected_scopes" + ], + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns the app's narrowed access.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NarrowedAppGrant" + } + } + }, + "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": "- `validation_error`: the body has an invalid or unknown field.\n- `missing_required_field`: a required field is absent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MissingRequiredFieldOrValidationErrorRefusal" + } + } + }, + "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`: direct recent owner or private machine authority is required, or the requested scopes widen access.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenRefusal" + } + } + }, + "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": "- `oauth_grant_not_found`: the ID does not name a live app grant.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OauthGrantNotFoundRefusal" + } + } + }, + "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 grant's scopes changed; read them again before submitting.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + } + }, + "delete": { + "operationId": "revokeAppGrant", + "tags": [ + "Apps" + ], + "security": [ + { + "bearerAuth": [] + }, + { + "ownerSession": [] + } + ], + "summary": "Revoke an app grant", + "description": "Revokes the app's tokens and consent. Requires `grants.manage` or direct authority; an owner browser needs a sign-in within five minutes.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the app's grant." + }, + "required": true, + "description": "The ID of the app's grant.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "`true` or `1` also revokes keys the app minted. Every other value, or omission, leaves those keys working." + }, + "required": false, + "description": "`true` or `1` also revokes keys the app minted. Every other value, or omission, leaves those keys working.", + "name": "revoke_keys", + "in": "query" + } + ], + "responses": { + "204": { + "description": "The app's grant is revoked.", + "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": "- `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": { - "type": "object", - "properties": { - "error": { - "type": "string", - "enum": [ - "invalid_token" - ], - "description": "The OAuth error code." - }, - "error_description": { - "type": "string", - "description": "A description of the error for a person to read." - } - }, - "required": [ - "error" - ] + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -46294,15 +47218,18 @@ }, "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.", + "403": { + "description": "- `forbidden`: the credential lacks `grants.manage`, or the owner must confirm a recent sign-in.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RequestTooLargeRefusal" + "$ref": "#/components/schemas/ForbiddenRefusal" } } }, @@ -46313,34 +47240,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "415": { - "description": "An error with `message` and `code`, rather than Marfa's own shape:\n- `UNSUPPORTED_MEDIA_TYPE`: the request doesn't send its body as `application/json`.", + "404": { + "description": "- `oauth_grant_not_found`: the ID does not name an app grant.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": [ - "UNSUPPORTED_MEDIA_TYPE" - ], - "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." - } - }, - "required": [ - "code", - "message" - ] + "$ref": "#/components/schemas/OauthGrantNotFoundRefusal" } } }, @@ -46359,6 +47278,30 @@ }, "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" } } }, @@ -46395,6 +47338,36 @@ } } }, + "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": { @@ -46424,33 +47397,60 @@ "$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" + } + } } } } }, - "/auth/grants": { + "/owner": { "get": { - "operationId": "listAppGrants", + "operationId": "getOwner", "tags": [ - "Apps" + "Owner" ], + "summary": "Get the owner", "security": [ - { - "bearerAuth": [] - }, { "ownerSession": [] } ], - "summary": "List app grants", - "description": "Returns every active app grant in one page. Requires `grants.manage` or direct authority.", + "description": "Returns the owner of the claimed instance. Requires a direct owner sign-in or local process authority.", "responses": { "200": { - "description": "Returns every active app grant.", + "description": "The owner.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AppGrantList" + "$ref": "#/components/schemas/Owner" } } }, @@ -46533,7 +47533,7 @@ } }, "403": { - "description": "- `forbidden`: the credential lacks `grants.manage`.", + "description": "This operation requires direct owner or local authority.", "content": { "application/json": { "schema": { @@ -46562,6 +47562,36 @@ } } }, + "404": { + "description": "The owner is unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnerNotFoundRefusal" + } + } + }, + "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": { @@ -46686,33 +47716,19 @@ } } } - } - }, - "/auth/grants/{id}": { + }, "patch": { - "operationId": "narrowAppGrant", + "operationId": "updateOwner", "tags": [ - "Apps" + "Owner" ], + "summary": "Update the owner", "security": [ { "ownerSession": [] } ], - "summary": "Narrow an app grant", - "description": "Narrows standing access. Requires a recent owner sign-in or private authority. Overbroad tokens, their refresh chains and pending codes end; covered chains keep working. The app may need to reconnect. Minted keys keep working.", - "parameters": [ - { - "schema": { - "type": "string", - "description": "The ID of the app's grant." - }, - "required": true, - "description": "The ID of the app's grant.", - "name": "id", - "in": "path" - } - ], + "description": "Updates the owner's email or name, or removes the picture. Requires a direct owner sign-in within five minutes or private machine authority. Changing email needs no confirmation; credentials, sessions, and grants keep their identity.", "requestBody": { "required": true, "content": { @@ -46720,28 +47736,23 @@ "schema": { "type": "object", "properties": { - "scopes": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "maxItems": 1000, - "description": "The complete resulting scope set, no wider than the current grant." + "email": { + "type": "string", + "maxLength": 254, + "format": "email", + "description": "Email address used to sign in. Stored in lowercase; changing it clears email verification." }, - "expected_scopes": { - "type": "array", - "items": { - "type": "string" - }, - "maxItems": 1000, - "description": "The complete scope set last read from the grant. A changed grant refuses the update." + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Display name, without control or bidirectional formatting characters. Spaces at each end are trimmed." + }, + "avatar": { + "type": "null", + "description": "Set `avatar` to `null` to remove the owner's picture, or omit it to leave it unchanged. To set a picture, send its bytes to `PUT /owner/picture` or `PUT /profile/picture`." } }, - "required": [ - "scopes", - "expected_scopes" - ], "additionalProperties": false } } @@ -46749,11 +47760,11 @@ }, "responses": { "200": { - "description": "Returns the app's narrowed access.", + "description": "The updated owner.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NarrowedAppGrant" + "$ref": "#/components/schemas/Owner" } } }, @@ -46776,7 +47787,7 @@ } }, "400": { - "description": "- `validation_error`: the body has an invalid or unknown field.\n- `missing_required_field`: a required field is absent.", + "description": "The body is empty, has an unknown field, or a field is invalid.", "content": { "application/json": { "schema": { @@ -46836,7 +47847,7 @@ } }, "403": { - "description": "- `forbidden`: direct recent owner or private machine authority is required, or the requested scopes widen access.", + "description": "The request needs private machine authority or direct owner authentication within five minutes.", "content": { "application/json": { "schema": { @@ -46866,11 +47877,11 @@ } }, "404": { - "description": "- `oauth_grant_not_found`: the ID does not name a live app grant.", + "description": "The owner is unavailable.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OauthGrantNotFoundRefusal" + "$ref": "#/components/schemas/OwnerNotFoundRefusal" } } }, @@ -46896,11 +47907,11 @@ } }, "409": { - "description": "- `conflict`: the grant's scopes changed; read them again before submitting.", + "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/ConflictRefusal" + "$ref": "#/components/schemas/InstanceChangedRefusal" } } }, @@ -47069,72 +48080,88 @@ } } } - } - }, - "delete": { - "operationId": "revokeAppGrant", - "tags": [ - "Apps" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "ownerSession": [] - } - ], - "summary": "Revoke an app grant", - "description": "Revokes the app's tokens and consent. Requires `grants.manage` or direct authority; an owner browser needs a sign-in within five minutes.", + }, "parameters": [ { + "name": "X-Marfa-Instance-Id", + "in": "header", + "required": false, "schema": { "type": "string", - "description": "The ID of the app's grant." - }, - "required": true, - "description": "The ID of the app's grant.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "string", - "description": "`true` or `1` also revokes keys the app minted. Every other value, or omission, leaves those keys working." + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "required": false, - "description": "`true` or `1` also revokes keys the app minted. Every other value, or omission, leaves those keys working.", - "name": "revoke_keys", - "in": "query" + "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." } + ] + }, + "post": { + "operationId": "createOwner", + "tags": [ + "Owner" ], + "summary": "Claim the instance", + "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": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "maxLength": 254, + "format": "email", + "description": "Email address the owner uses to sign in." + }, + "password": { + "type": "string", + "description": "Password for the new owner." + }, + "name": { + "type": "string", + "maxLength": 200, + "description": "Display name. Defaults to the part before @ in the email address when omitted or blank." + }, + "code": { + "type": "string", + "description": "Machine-issued setup code. Omit when using a setup-only browser session." + } + }, + "required": [ + "email", + "password" + ], + "additionalProperties": false + } + } + } + }, "responses": { - "204": { - "description": "The app's grant is revoked.", + "201": { + "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": { + "$ref": "#/components/schemas/Owner" + } + } + }, "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": "- `validation_error`: the query has a parameter this endpoint doesn't take.", + "description": "Owner details are invalid, or the body has a field this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/MissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -47145,26 +48172,17 @@ "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.", + "description": "Setup proof is invalid, expired, or replaced.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/SetupProofInvalidRefusal" } } }, @@ -47175,22 +48193,13 @@ "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`: the credential lacks `grants.manage`, or the owner must confirm a recent sign-in.", + "description": "The browser origin or transport is not permitted.", "content": { "application/json": { "schema": { @@ -47205,26 +48214,24 @@ "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": "- `oauth_grant_not_found`: the ID does not name an app grant.", + "409": { + "description": "The instance has already been claimed.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OauthGrantNotFoundRefusal" + "anyOf": [ + { + "$ref": "#/components/schemas/OwnerExistsRefusal" + }, + { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + ] } } }, @@ -47235,15 +48242,6 @@ "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" } @@ -47271,7 +48269,7 @@ } }, "429": { - "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "description": "The address has used its setup-code attempts.", "content": { "application/json": { "schema": { @@ -47286,15 +48284,6 @@ "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" }, @@ -47319,26 +48308,69 @@ "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.", + } + }, + "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." + } + ] + } + }, + "/profile": { + "patch": { + "operationId": "updateProfile", + "tags": [ + "Owner" + ], + "summary": "Update the owner's profile", + "description": "Updates the owner's name or removes the picture and returns `ok: true`. Requires profile write permission for every submitted field; email cannot be changed here. All fields commit together.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Display name, without control or bidirectional formatting characters. Spaces at each end are trimmed." + }, + "avatar": { + "type": "null", + "description": "Set `avatar` to `null` to remove the owner's picture, or omit it to leave it unchanged. To set a picture, send its bytes to `PUT /owner/picture` or `PUT /profile/picture`." + } + }, + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns ok: true. The profile was updated.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WriteContentionRefusal" + "$ref": "#/components/schemas/Ok" } } }, @@ -47357,18 +48389,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": "The body is empty, contains an unknown field, or a field is invalid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InsufficientStorageRefusal" + "$ref": "#/components/schemas/MissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -47392,30 +48421,13 @@ "$ref": "#/components/headers/X-Error-Code" } } - } - } - } - }, - "/owner": { - "get": { - "operationId": "getOwner", - "tags": [ - "Owner" - ], - "summary": "Get the owner", - "security": [ - { - "ownerSession": [] - } - ], - "description": "Returns the owner of the claimed instance. Requires a direct owner sign-in or local process authority.", - "responses": { - "200": { - "description": "The owner.", + }, + "401": { + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -47434,15 +48446,18 @@ }, "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" } } }, - "400": { - "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", + "403": { + "description": "The credential lacks profile write permission for a submitted field.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationErrorRefusal" + "$ref": "#/components/schemas/ForbiddenRefusal" } } }, @@ -47467,12 +48482,12 @@ } } }, - "401": { - "description": "`unauthorized`: the request has no credential, or its credential is not valid.", + "404": { + "description": "The owner is unavailable.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedRefusal" + "$ref": "#/components/schemas/OwnerNotFoundRefusal" } } }, @@ -47497,12 +48512,12 @@ } } }, - "403": { - "description": "This operation requires direct owner or local authority.", + "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/ForbiddenRefusal" + "$ref": "#/components/schemas/InstanceChangedRefusal" } } }, @@ -47527,12 +48542,12 @@ } } }, - "404": { - "description": "The owner is unavailable.", + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OwnerNotFoundRefusal" + "$ref": "#/components/schemas/RequestTooLargeRefusal" } } }, @@ -47543,15 +48558,6 @@ "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" } @@ -47680,57 +48686,49 @@ } } } - } - }, - "patch": { - "operationId": "updateOwner", + }, + "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." + } + ] + } + }, + "/owner/picture": { + "put": { + "operationId": "replaceOwnerPicture", "tags": [ "Owner" ], - "summary": "Update the owner", + "summary": "Replace the owner's picture", "security": [ { "ownerSession": [] } ], - "description": "Updates the owner's email, name, or picture URL and returns the owner. Requires a direct owner sign-in within five minutes or private machine authority. Changing email needs no confirmation; credentials, sessions, and grants keep their identity.", + "description": "Stores the request body as the owner's picture and returns the owner. Requires a direct owner sign-in within five minutes or private machine authority.", "requestBody": { "required": true, + "description": "The picture's bytes, sent as they are, not as `multipart/form-data`. Marfa decides the kind from the bytes and ignores `Content-Type` and any file name. A PNG, JPEG or WebP of 2 MiB or less, 1 to 4096 pixels on each side.", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "maxLength": 254, - "format": "email", - "description": "Email address used to sign in. Stored in lowercase; changing it clears email verification." - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Display name, without control or bidirectional formatting characters. Spaces at each end are trimmed." - }, - "avatar": { - "type": [ - "string", - "null" - ], - "maxLength": 2048, - "format": "uri", - "description": "The owner's picture URL. Use HTTP or HTTPS. Set `avatar` to `null` to clear it, or omit it to leave it unchanged. Marfa does not fetch the URL." - } - }, - "additionalProperties": false + "type": "string", + "format": "binary" } } } }, "responses": { "200": { - "description": "The updated owner.", + "description": "The updated owner, whose `avatar` is the picture's address.", "content": { "application/json": { "schema": { @@ -47757,11 +48755,11 @@ } }, "400": { - "description": "The body is empty, has an unknown field, or a field is invalid.", + "description": "The body is empty, damaged, not a PNG, JPEG or WebP, or larger than 4096 pixels on a side. For example, a GIF, an AVIF or an SVG.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -48064,74 +49062,100 @@ } ] }, - "post": { - "operationId": "createOwner", + "get": { + "operationId": "getOwnerPicture", "tags": [ "Owner" ], - "summary": "Claim the instance", - "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": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "maxLength": 254, - "format": "email", - "description": "Email address the owner uses to sign in." - }, - "password": { - "type": "string", - "description": "Password for the new owner." - }, - "name": { - "type": "string", - "maxLength": 200, - "description": "Display name. Defaults to the part before @ in the email address when omitted or blank." - }, - "code": { - "type": "string", - "description": "Machine-issued setup code. Omit when using a setup-only browser session." - } - }, - "required": [ - "email", - "password" - ], - "additionalProperties": false - } - } + "summary": "Get the owner's picture", + "security": [ + { + "ownerSession": [] + }, + { + "bearerAuth": [] } - }, + ], + "description": "Returns the owner's picture as an image download. Requires an owner sign-in or a credential with profile read permission for the avatar. Send `If-None-Match` to revalidate.", "responses": { - "201": { - "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.", + "200": { + "description": "Returns the picture's bytes.", + "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" + }, + "ETag": { + "schema": { + "type": "string" + }, + "description": "The quoted SHA-256 of the picture's bytes." + } + }, "content": { - "application/json": { + "image/png": { "schema": { - "$ref": "#/components/schemas/Owner" + "type": "string", + "format": "binary" + } + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "image/webp": { + "schema": { + "type": "string", + "format": "binary" } } - }, + } + }, + "304": { + "description": "The picture still matches the `If-None-Match` value.", "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" + }, + "ETag": { + "schema": { + "type": "string" + }, + "description": "The quoted SHA-256 of the picture's bytes." } } }, "400": { - "description": "Owner details are invalid, or the body has a field this endpoint doesn't take.", + "description": "- `validation_error`: the query has a parameter this endpoint doesn't take.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -48142,17 +49166,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, "401": { - "description": "Setup proof is invalid, expired, or replaced.", + "description": "`unauthorized`: the request has no credential, or its credential is not valid.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetupProofInvalidRefusal" + "$ref": "#/components/schemas/UnauthorizedRefusal" } } }, @@ -48163,13 +49196,22 @@ "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": "The browser origin or transport is not permitted.", + "description": "The credential lacks profile read permission for the avatar.", "content": { "application/json": { "schema": { @@ -48184,24 +49226,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "409": { - "description": "The instance has already been claimed.", + "404": { + "description": "No picture is set.", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/OwnerExistsRefusal" - }, - { - "$ref": "#/components/schemas/InstanceChangedRefusal" - } - ] + "$ref": "#/components/schemas/NotFoundRefusal" } } }, @@ -48212,17 +49256,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" } } }, - "413": { - "description": "`request_too_large`: the request body is larger than this instance accepts.", + "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/RequestTooLargeRefusal" + "$ref": "#/components/schemas/RateLimitedRefusal" } } }, @@ -48233,17 +49286,29 @@ "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" } } }, - "429": { - "description": "The address has used its setup-code attempts.", + "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/RateLimitedRefusal" + "$ref": "#/components/schemas/InternalErrorRefusal" } } }, @@ -48254,20 +49319,26 @@ "X-Request-ID": { "$ref": "#/components/headers/X-Request-ID" }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, "X-Error-Code": { "$ref": "#/components/headers/X-Error-Code" - }, - "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.", + "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/InternalErrorRefusal" + "$ref": "#/components/schemas/WriteContentionRefusal" } } }, @@ -48278,70 +49349,81 @@ "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-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}$" + }, + "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" + } + } }, - "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." + "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" + } + } } - ] + } } }, - "/profile": { - "patch": { - "operationId": "updateProfile", + "/profile/picture": { + "put": { + "operationId": "replaceProfilePicture", "tags": [ "Owner" ], - "summary": "Update the owner's profile", - "description": "Updates the owner's name or avatar and returns `ok: true`. Requires profile write permission for every submitted field; email cannot be changed here. All fields commit together.", + "summary": "Replace the picture as an app", "security": [ { "bearerAuth": [] } ], + "description": "Stores the request body as the owner's picture and returns `ok: true`. Requires profile write permission for the avatar.", "requestBody": { "required": true, + "description": "The picture's bytes, sent as they are, not as `multipart/form-data`. Marfa decides the kind from the bytes and ignores `Content-Type` and any file name. A PNG, JPEG or WebP of 2 MiB or less, 1 to 4096 pixels on each side.", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Display name, without control or bidirectional formatting characters. Spaces at each end are trimmed." - }, - "avatar": { - "type": [ - "string", - "null" - ], - "maxLength": 2048, - "format": "uri", - "description": "The owner's picture URL. Use HTTP or HTTPS. Set `avatar` to `null` to clear it, or omit it to leave it unchanged. Marfa does not fetch the URL." - } - }, - "additionalProperties": false + "type": "string", + "format": "binary" } } } }, "responses": { "200": { - "description": "Returns ok: true. The profile was updated.", + "description": "Returns ok: true. The picture was stored.", "content": { "application/json": { "schema": { @@ -48368,11 +49450,11 @@ } }, "400": { - "description": "The body is empty, contains an unknown field, or a field is invalid.", + "description": "The body is empty, damaged, not a PNG, JPEG or WebP, or larger than 4096 pixels on a side. For example, a GIF, an AVIF or an SVG.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/ValidationErrorRefusal" } } }, @@ -48428,7 +49510,7 @@ } }, "403": { - "description": "The credential lacks profile write permission for a submitted field.", + "description": "The credential lacks profile write permission for the avatar.", "content": { "application/json": { "schema": { @@ -55290,7 +56372,10 @@ "createOwner", "getOwner", "updateOwner", - "updateProfile" + "updateProfile", + "replaceOwnerPicture", + "replaceProfilePicture", + "getOwnerPicture" ] }, { diff --git a/src/generated/cli-commands.json b/src/generated/cli-commands.json index 2abe1c32..4775bd0f 100644 --- a/src/generated/cli-commands.json +++ b/src/generated/cli-commands.json @@ -21,6 +21,7 @@ "owner passkeys add": {"options": [{"flag": "--no-browser"}]}, "owner recover": {"options": [{"flag": "--stdin"}]}, "owner update": {"options": [{"flag": "--email", "value": true}, {"flag": "--name", "value": true}, {"flag": "--avatar", "value": true}, {"flag": "--clear-avatar"}]}, + "owner picture": {"options": [{"flag": "--output", "value": true}]}, "profile": {"options": [], "subcommands": true}, "profile update": {"options": [{"flag": "--name", "value": true}, {"flag": "--avatar", "value": true}, {"flag": "--clear-avatar"}]}, "apps": {"options": [], "subcommands": true}, diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index 37ca75b0..eb7cbe4e 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.25/marfa-0.0.25-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.26/marfa-0.0.26-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 761e082e..230f8b89 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -749,6 +749,13 @@ "device/wrong-schema-says": "2d5673c280a0de5c", "device/wrong-schema-names-kind": "29f0d20d00f88c4b", "device/wrong-schema-no-delete": "3cb3cfa4cc6ef74c", + "device/replace-refused": "b09a07ee01121076", + "device/replace-refused-opens": "fe41a6553ce3af70", + "device/replace-refused-unsent": "cd2500a421a937c5", + "device/replace-refused-says": "d41c41786f70e043", + "device/replace-refused-held": "3496338e6706add1", + "device/replace-refused-usable": "1a0ad319f329ac82", + "device/replace-refused-kept": "ec5dfb4024164f30", "device/writer-per-file": "623e1989deb95557", "device/writer-lock-unwritable": "12f35aa7c2c2f21e", "device/command-writer-lock-unwritable": "fdd393377cd321cd", @@ -2950,15 +2957,37 @@ "instance-claim/recovery-keeps-keys": "2cc3ab5fddb9a646", "instance-claim/management-metrics": "4584ce7fe3e4de05", "instance-claim/control-only-recovery": "81b8c5f27479b583", - "instance-claim/profile-owner-unavailable": "9e35b62bb9862e97", - "instance-claim/owner-update": "35679e7923aa2803", - "instance-claim/owner-avatar": "7f7b532e23cebbb5", + "instance-claim/profile-owner-unavailable": "4fd6f8c7fcb2b776", + "instance-claim/owner-update": "034a30291b8e245b", + "instance-claim/owner-avatar": "828d845408e485b4", "instance-claim/owner-update-authority": "7fe8749088555d71", "instance-claim/owner-update-identity": "122246dd8ed9f163", "instance-claim/owner-update-email": "ea5f7bb9e9351e19", - "instance-claim/profile-input": "aca334a7213b29e2", - "instance-claim/delegated-profile-update": "283a88306f44b57c", + "instance-claim/profile-input": "4ce6fa2ab0dd64ee", + "instance-claim/delegated-profile-update": "7b725c5fe21fbedf", "instance-claim/delegated-profile-atomic": "485da85f0b99d472", + "instance-claim/owner-picture-upload": "953f0fe7944dd60f", + "instance-claim/profile-picture-upload": "cec2177f6830c7cc", + "instance-claim/owner-picture-authority": "cc3c0f249fa8431a", + "instance-claim/profile-picture-authority": "ebe2d5e12825fb31", + "instance-claim/picture-kind-from-bytes": "bfb5eb6e49df951e", + "instance-claim/picture-kind-refused": "f6ad4fe9f783e6b5", + "instance-claim/picture-empty": "d126f2373b10b83a", + "instance-claim/picture-damaged": "dafbb1f228e60691", + "instance-claim/picture-dimensions": "6a3c590f7092e3b8", + "instance-claim/picture-size": "c1a4501e539070a8", + "instance-claim/picture-size-early": "318177fa8d6e4142", + "instance-claim/picture-interrupted": "bb72259b631c0ff0", + "instance-claim/picture-replace": "119fb5fc6a2ade89", + "instance-claim/owner-avatar-address": "42662b03c4eb7b32", + "instance-claim/picture-claim": "6a007f00b005b686", + "instance-claim/picture-claim-none": "59f34494354dba4e", + "instance-claim/picture-served": "42e88df8951fc2e7", + "instance-claim/picture-served-headers": "8f0b28b34374f029", + "instance-claim/picture-validators": "27a38139a805e586", + "instance-claim/picture-etag": "f58a5fb7e3b38761", + "instance-claim/picture-read-authority": "4e6d41e20dead404", + "instance-claim/picture-none": "cf3da4ab0f2f7efe", "instance-claim/profile-email-owner-only": "34cbf8c80b12c31d", "instance-claim/profile-grant-closed": "74b7b94f0d9ca6de", "instance-claim/owner-update-email-unverified": "f10d1b6930e4df1c", @@ -3077,6 +3106,8 @@ "instance/page-policy": "e1a892a64626051b", "instance/page-policy-nonce-only": "259228eb3bbefd69", "instance/page-policy-default-none": "f79682f7197d8b42", + "instance/page-policy-images-console": "72262b7390cc2114", + "instance/page-policy-images-other": "b0830b27e0948455", "instance/page-policy-json-none": "a2b94c9a96a5ce61", "instance/page-nonce-fresh": "5021fd6d47257f2a", "instance/page-nonce-carried": "4aedb96f6a9535f2", diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index 09f1d05c..79e7339c 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.25"} +{"version":"v0.0.26"} diff --git a/src/generated/server-image.md b/src/generated/server-image.md index 468131ae..a3b64a39 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.25 +docker pull ghcr.io/withmarfa/marfa-server:0.0.26 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.25 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.26 ``` From bda9c5ce722e4f0b7f5eee903a3318cdcc6e3c4c Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Thu, 22 Oct 2026 19:27:53 +0100 Subject: [PATCH 2/3] docs(release): align the pages with marfa v0.0.26 --- content/docs/(overview)/access.mdx | 4 +-- content/docs/(overview)/devices.mdx | 2 +- content/docs/(reference)/changelog.mdx | 27 ++++++++++++++++++++ content/docs/(reference)/cli.mdx | 4 ++- content/docs/catalog/permissions.mdx | 6 ++--- content/docs/guides/export-and-restore.mdx | 2 +- content/docs/guides/install.mdx | 8 +++++- content/docs/guides/sign-in-as-the-owner.mdx | 16 +++++++++--- content/docs/guides/troubleshooting.mdx | 2 +- content/docs/guides/upgrade.mdx | 2 +- 10 files changed, 59 insertions(+), 14 deletions(-) diff --git a/content/docs/(overview)/access.mdx b/content/docs/(overview)/access.mdx index 19b9df17..455a3685 100644 --- a/content/docs/(overview)/access.mdx +++ b/content/docs/(overview)/access.mdx @@ -37,14 +37,14 @@ A map names things at a level, `read` or `write`, and `write` includes reading. | Edge | `edge_permissions` | Edge types | `"about": "read"` | | Extension | `extension_permissions` | Extension namespaces | `"com.example.reader": "write"` | | Metadata | `metadata_permissions` | Registering types and edge types | `"types": "write"` | -| Profile | `profile_permissions` | The owner's name, email and avatar | `"email": "read"` | +| Profile | `profile_permissions` | The owner's name, email and picture | `"email": "read"` | A few rules apply to single maps: - **The type map** matters most. Only it takes a third level, `none`, which shuts out a type its wildcard covers. - **The metadata map** covers the definitions on your instance, not an item's tags and extensions. - **The extension map** is a key's alone: an app reaches no extensions. -- **The profile map** can write only the owner's name and avatar, never the sign-in email. +- **The profile map** can write only the owner's name and picture, never the sign-in email. ## What a refusal reveals diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index d099a724..7e89e6d1 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -15,7 +15,7 @@ Reads come from the copy, with no connection needed. A file's bytes are there on Sometimes a copy can't be brought up to date, for example after the device was away longer than the instance keeps events. The device then fills its copy again from scratch, and its **queued writes survive**. -A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do. +A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do. An app built on the core's Node or Swift binding can replace a refused copy that holds nothing unsent with an empty one. ## A write made offline diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index e1d4782f..5e04ef30 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -7,6 +7,33 @@ Each release of Marfa ships the server and its container image, the CLI for macO **Marfa doesn't 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; a version change alone doesn't cause a refusal. [Upgrade](/guides/upgrade) shows how to test an export and restore on fresh storage before switching builds. +## v0.0.26 + +Released October 22, 2026. [v0.0.26 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.26) + +**This release changes the database's shape, because the owner's picture is now a file the owner uploads and never a web address.** It refuses an existing v0.0.25 database without changing it: the `auth_user` table no longer has an `image` column, and a new `owner_picture` table holds the picture. Nothing is migrated. To move an instance that holds data, export it with v0.0.25, restore the export into a fresh v0.0.26 instance and upload the owner's picture again, because an archive doesn't carry it. [Upgrade](/guides/upgrade) covers the process. + +For apps and the API reference (breaking): + +- **Three operations store and serve the owner's picture.** [`PUT /owner/picture`](/api-reference/owner/replaceOwnerPicture) takes the picture from a direct owner sign-in of the last five minutes or from the private socket. [`PUT /profile/picture`](/api-reference/owner/replaceProfilePicture) takes it from a key or an app with profile write access for `avatar`, and answers `{ "ok": true }`. [`GET /owner/picture`](/api-reference/owner/getOwnerPicture) returns the bytes to the signed-in owner and to a credential with profile read access for `avatar`, and answers `404 not_found` while there is no picture. The body of an upload is the picture's bytes, sent as they are and not as `multipart/form-data`: a PNG, JPEG or WebP of 2 MiB or less, between 1 and 4096 pixels on each side. Marfa decides the kind from the bytes and ignores `Content-Type` and the file name. A file of another kind, an empty or damaged one, or one past 4096 pixels answers `400 validation_error`, one past 2 MiB answers `413 request_too_large`, and either leaves the previous picture in place. +- **`avatar` takes only `null` in an update.** [`PATCH /owner`](/api-reference/owner/updateOwner) and [`PATCH /profile`](/api-reference/owner/updateProfile) remove the picture when `avatar` is `null`. A web address, or any other value, answers `400 validation_error` and changes nothing. An app that set `avatar` to an address must upload the picture instead. +- **The owner's `avatar` is the picture's address on the instance.** It reads `https:///owner/picture` while there is a picture and `null` while there is none, and the OIDC `picture` claim of an app that may read the avatar carries the same address. The picture isn't public: fetch it with the credential, as `GET /owner/picture` requires. + +For using the CLI (breaking): + +- **`--avatar` takes a picture file.** `marfa owner update --avatar ` and `marfa profile update --avatar ` upload the file where they took a web address. `--clear-avatar` removes the picture. The CLI refuses a path that isn't a file, or a file over 2 MiB, before it sends anything, and it sends the picture before the name or email, so a refusal leaves those as they were. +- **`marfa owner picture` saves the owner's picture.** `--output ` writes it to a file, and without the option the bytes go to standard output. +- **`marfa server install` says where a tunnel points.** For `cloudflare` and `proxy`, it names the address on this machine that the tunnel or proxy must target, and its summary repeats it as **Point at**, because a tunnel aimed anywhere else answers `502`. The summary names the machine that holds the folder and ends by pointing at `marfa docs guides/ai-agents`. With `--json`, the report gains `host` and `local_address`. `marfa --help` now ends by saying where the docs are. See [Choose how it's reached](/guides/install#choose-how-its-reached). + +For the owner's console: + +- **Profile uploads and removes the picture.** Under **Picture**, **Change** opens a file chooser for a PNG, JPEG or WebP of 2 MiB or less, and **Remove picture** removes it. [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) covers it. +- **List rows no longer underline when you point at them.** + +For folders, apps and devices: + +- **The core's Node and Swift bindings can replace a refused working copy.** `replaceRefused` sets a file that `open` refuses with `wrong_schema` aside beside it and opens an empty copy, but only when the file holds no write the server hasn't taken. What only the old file held, such as pins, isn't carried. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers it. + ## v0.0.25 Released October 22, 2026. [v0.0.25 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.25) diff --git a/content/docs/(reference)/cli.mdx b/content/docs/(reference)/cli.mdx index 9e353ace..cc131a84 100644 --- a/content/docs/(reference)/cli.mdx +++ b/content/docs/(reference)/cli.mdx @@ -172,12 +172,14 @@ That's how the owner makes the first key in the [Quickstart](/quickstart). These | --- | --- | --- | | `marfa setup status`, `setup code`, `setup open` | Check whether the instance is claimed, replace the setup code, and open the browser handoff. | [Quickstart](/quickstart) | | `marfa owner recover` | Set a new owner password. | [Sign in as the owner](/guides/sign-in-as-the-owner#recover-the-owners-password) | -| `marfa owner update` | Change the owner's email, name and picture address. | [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) | +| `marfa owner update` | Change the owner's email and name, upload the owner's picture from a file with `--avatar`, or remove it with `--clear-avatar`. | [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) | | `marfa owner passkeys` | Add, list, rename and remove passkeys. | [Sign in as the owner](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) | | `marfa items summaries` | List item summaries without opening item content. | [Access](/access#who-can-act) | | `marfa items trash` | List, move, restore and purge items in the trash. | [Housekeeping](/guides/housekeeping#empty-the-bin) | | `marfa sign-ins` | List, rename and end the sign-ins that can reach the server. | [Sign in as the owner](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) | +`marfa owner picture` writes the owner's picture to the file named by `--output`, or its bytes to standard output, and a key that may read the profile's `avatar` can run it too. A key or an app with profile write access changes the name and the picture with `marfa profile update`, which takes `--name`, `--avatar ` and `--clear-avatar`. [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) shows both. + **The socket carries no key.** `--socket` can't be combined with `--url`, `--key`, `MARFA_API_URL` or `MARFA_API_KEY`. An operation that needs a key, such as `marfa events` or `marfa export`, ends with exit code 5, so run it with a key instead. `marfa login`, `logout`, `device`, `docs`, `types scaffold` and local folder commands refuse `--socket`. `marfa folders inventory` is the exception: it reads the owner’s reported folder copies through the socket. ## Exit codes diff --git a/content/docs/catalog/permissions.mdx b/content/docs/catalog/permissions.mdx index fc4c1c30..a92721c5 100644 --- a/content/docs/catalog/permissions.mdx +++ b/content/docs/catalog/permissions.mdx @@ -27,9 +27,9 @@ A key's `profile_permissions` names `name`, `email`, `avatar`, or `*`. The wildc | --- | --- | --- | | `name` | `read`, `write` | The owner's display name. | | `email` | `read` | Nothing. Only the owner can change the sign-in address. | -| `avatar` | `read`, `write` | The avatar URL, including clearing it. | -| `*` | `read`, `write` | Name and avatar only; email stays read-only. | +| `avatar` | `read`, `write` | The owner's picture: uploading a new one and removing it. | +| `*` | `read`, `write` | Name and picture only; email stays read-only. | -Profile write includes read. An explicit key grant for email write is refused, as is the OAuth scope `profile.email:write`. OAuth uses `profile.name:write` and `profile.avatar:write` for individual fields, or `profile:write` for both ([owner and delegated profile changes](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance-claim.md#instance-claimprofile-grant-closed)). +Profile write includes read. Read access to `avatar` lets a credential fetch the picture with [`GET /owner/picture`](/api-reference/owner/getOwnerPicture). An explicit key grant for email write is refused, as is the OAuth scope `profile.email:write`. OAuth uses `profile.name:write` and `profile.avatar:write` for individual fields, or `profile:write` for both ([owner and delegated profile changes](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance-claim.md#instance-claimprofile-grant-closed)). [Access](/access) explains the maps, and [Manage keys](/guides/manage-keys) shows how to give a key its access. diff --git a/content/docs/guides/export-and-restore.mdx b/content/docs/guides/export-and-restore.mdx index a83b07d2..73dd9ea6 100644 --- a/content/docs/guides/export-and-restore.mdx +++ b/content/docs/guides/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 or audit log. It can move your data to a new build when you [upgrade](/guides/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, 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](/guides/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](/guides/back-up-and-restore) instead. diff --git a/content/docs/guides/install.mdx b/content/docs/guides/install.mdx index 718a0b68..fc01d39b 100644 --- a/content/docs/guides/install.mdx +++ b/content/docs/guides/install.mdx @@ -19,6 +19,8 @@ marfa server install ``` ```text +Options, all optional: --dir to install somewhere else, --port for the port on this machine, --name to run more than one, --external-settings when a secrets manager writes marfa.env. `marfa server install --help` lists them all. + Checking Docker... How will your devices reach this server? 1. local: only this machine, at http://localhost @@ -44,7 +46,7 @@ Setting the server to start when you log in... Marfa is running. Address http://localhost:8600 - Folder /Users/you/marfa-server + Folder /Users/you/marfa-server on your-mac Backup none: no copy leaves this machine Keep a copy of /Users/you/marfa-server/marfa.env somewhere safe, outside this folder. It holds the two secrets the data needs, and they are not stored anywhere else. @@ -54,6 +56,8 @@ Open this link to choose the owner's email and password. It works once, for five http://localhost:8600/setup#handoff=… It starts when you log in, starting Docker first if it is not running, and again after a failure. `marfa server status` shows it. + +To give an AI agent access to this server, run `marfa docs guides/ai-agents`. ``` @@ -83,6 +87,8 @@ The way you reach the instance decides which caller address the server believes, The instance gets a Docker network of its own, named for the install (`marfa-net` unless you pass `--name`), and the command reads its range from Docker. The network is made once and kept when the container is replaced, so the range stays the one the setting names. The port is 8600, or the next free one, unless you pass `--port`. It is published on the loopback address only, so only a proxy or tunnel on the same machine reaches it. +**For `cloudflare` and `proxy`, point the tunnel or proxy at `http://localhost:` and the port, on the machine that holds the folder.** The command says so when it asks for the address, and its summary repeats it as **Point at** beside the machine's name. A tunnel aimed at another port, or running on another machine, makes the address answer `502`. + ## What the command writes The command keeps everything in one folder, `marfa-server` in your home directory unless you pass `--dir`: diff --git a/content/docs/guides/sign-in-as-the-owner.mdx b/content/docs/guides/sign-in-as-the-owner.mdx index 7cd5af9d..5c2b530b 100644 --- a/content/docs/guides/sign-in-as-the-owner.mdx +++ b/content/docs/guides/sign-in-as-the-owner.mdx @@ -35,7 +35,7 @@ The list gives each sign-in's ID, kind, name and last use. A browser can't be re When your sign-in is older than five minutes, these changes show a **Confirm it's you** dialog that asks for the owner's password or a passkey, then send the change again: - Adding, renaming or removing a passkey. -- Changing the owner's name, email or picture address on the **Profile** page. +- Changing the owner's name, email or picture, or removing the picture, on the **Profile** page. - Making, changing or revoking a key on the **API keys** page. - Narrowing or disconnecting an app, renaming a sign-in, ending another sign-in, or signing out the other browsers or every browser. - Moving, restoring, purging or emptying items through **Trash**, removing an unused custom type, retrying a webhook delivery, or restoring an archive. @@ -78,9 +78,19 @@ Only the owner changes the sign-in email: marfa --socket /srv/marfa/data/control/marfa.sock owner update --email owner@example.com --name "Reader" ``` -In a browser, the **Profile** page uses [Update the owner](/api-reference/owner/updateOwner). Under **Picture**, choose **Change** to set an HTTP(S) picture address, or leave it blank to clear it. Marfa stores the address, not an uploaded picture. The email is stored in lowercase, needs no confirmation and becomes the address for password sign-in. Everything else the owner holds stays in place. +In a browser, the **Profile** page uses [Update the owner](/api-reference/owner/updateOwner). The email is stored in lowercase, needs no confirmation and becomes the address for password sign-in. Everything else the owner holds stays in place. -A key or an app with profile write access changes only the owner's **name or avatar**, with `marfa profile update --name "Reader"` or [Update the owner's profile](/api-reference/owner/updateProfile). **Email is never writable this way.** [Permissions](/catalog/permissions#access-to-profile-fields) lists the levels. +**The picture is a file you upload, never a web address.** It must be a PNG, JPEG or WebP of **2 MiB or less**, between 1 and 4096 pixels on each side. Marfa decides the kind from the file's bytes, ignores its name, and keeps the picture in its database. On the **Profile** page, choose **Change** under **Picture**, choose the file and select **Save**; **Remove picture** removes it. From a terminal, pass the file to `--avatar`: + +```bash +marfa --socket /srv/marfa/data/control/marfa.sock owner update --avatar ./me.png +marfa --socket /srv/marfa/data/control/marfa.sock owner picture --output ./saved.png +marfa --socket /srv/marfa/data/control/marfa.sock owner update --clear-avatar +``` + +The CLI refuses a path that isn't a file, or a file over 2 MiB, before it sends anything. A file of another kind, or one that is empty or damaged, is refused `400 validation_error` and the previous picture stays. An API client sends the bytes with [Replace the owner's picture](/api-reference/owner/replaceOwnerPicture) and reads them with [Get the owner's picture](/api-reference/owner/getOwnerPicture). + +A key or an app with profile write access changes only the owner's **name or picture**, with `marfa profile update --name "Reader"`, `marfa profile update --avatar ./me.png` or [Update the owner's profile](/api-reference/owner/updateProfile). An app uploads a picture with [Replace the picture as an app](/api-reference/owner/replaceProfilePicture), and an update can only remove one by setting `avatar` to `null`. **Email is never writable this way.** [Permissions](/catalog/permissions#access-to-profile-fields) lists the levels. ## Recover the owner's password diff --git a/content/docs/guides/troubleshooting.mdx b/content/docs/guides/troubleshooting.mdx index c496d8cb..262e2d0c 100644 --- a/content/docs/guides/troubleshooting.mdx +++ b/content/docs/guides/troubleshooting.mdx @@ -262,7 +262,7 @@ A working copy is a file that one build of the core made, and a build with a dif The message says how many writes the file holds that the server has not taken: waiting, blocked, refused or dead. Only the build that made the file can send or show them, so what you do depends on that count: -- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. +- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node or Swift binding can call `replaceRefused` instead: it sets the old file aside beside it and opens an empty copy, which holds none of what only the old file held, such as pins. - **Some.** Open the file with the build that made it, run `marfa device drain` to send the writes, and use `marfa device discard` on any it refuses that you don't want. Then hydrate a new copy with the newer build. Keep the old file until you have done this, because deleting it loses those writes. If some of the writes are of a kind the current build no longer queues, the message names the kind and how many, such as `1 write of kind merge_metadata`. That one is a tag merge, which an instance at v0.0.24 or later answers `404 not_found`. Read the tags it carries with `marfa device queue`, and add them with `marfa items tag`. diff --git a/content/docs/guides/upgrade.mdx b/content/docs/guides/upgrade.mdx index 818d48e5..91c71109 100644 --- a/content/docs/guides/upgrade.mdx +++ b/content/docs/guides/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, and set the configuration 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](/guides/manage-keys) and [Settings](/guides/settings) cover them. +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](/guides/manage-keys), [Settings](/guides/settings) and [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) cover them. From 92988cec778e7d10b62106dab0dcc67e34d7997e Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Thu, 22 Oct 2026 19:35:35 +0100 Subject: [PATCH 3/3] docs(release): correct what replaceRefused leaves and where the install hint points --- content/docs/(reference)/changelog.mdx | 4 ++-- content/docs/guides/install.mdx | 2 +- content/docs/guides/troubleshooting.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index 5e04ef30..2d312cb7 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -27,12 +27,12 @@ For using the CLI (breaking): For the owner's console: -- **Profile uploads and removes the picture.** Under **Picture**, **Change** opens a file chooser for a PNG, JPEG or WebP of 2 MiB or less, and **Remove picture** removes it. [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) covers it. +- **Profile uploads and removes the picture.** Under **Picture**, **Change** opens a dialog to choose a PNG, JPEG or WebP of 2 MiB or less, and **Remove picture** removes it. [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) covers it. - **List rows no longer underline when you point at them.** For folders, apps and devices: -- **The core's Node and Swift bindings can replace a refused working copy.** `replaceRefused` sets a file that `open` refuses with `wrong_schema` aside beside it and opens an empty copy, but only when the file holds no write the server hasn't taken. What only the old file held, such as pins, isn't carried. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers it. +- **The core's Node and Swift bindings can replace a refused working copy.** `replaceRefused` replaces a file that `open` refuses with `wrong_schema` by an empty copy and opens it, but only when the file holds no write the server hasn't taken. The old file is removed, so what only it held, such as pins, is lost. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers it. ## v0.0.25 diff --git a/content/docs/guides/install.mdx b/content/docs/guides/install.mdx index fc01d39b..458e6688 100644 --- a/content/docs/guides/install.mdx +++ b/content/docs/guides/install.mdx @@ -87,7 +87,7 @@ The way you reach the instance decides which caller address the server believes, The instance gets a Docker network of its own, named for the install (`marfa-net` unless you pass `--name`), and the command reads its range from Docker. The network is made once and kept when the container is replaced, so the range stays the one the setting names. The port is 8600, or the next free one, unless you pass `--port`. It is published on the loopback address only, so only a proxy or tunnel on the same machine reaches it. -**For `cloudflare` and `proxy`, point the tunnel or proxy at `http://localhost:` and the port, on the machine that holds the folder.** The command says so when it asks for the address, and its summary repeats it as **Point at** beside the machine's name. A tunnel aimed at another port, or running on another machine, makes the address answer `502`. +**For `cloudflare` and `proxy`, point the tunnel or proxy at `http://localhost:` and the port, on the machine that holds the folder.** The command says so when it asks for the address, and its summary repeats it as **Point at**, with the machine's name beside the folder. A tunnel aimed at another port, or running on another machine, makes the address answer `502`. ## What the command writes diff --git a/content/docs/guides/troubleshooting.mdx b/content/docs/guides/troubleshooting.mdx index 262e2d0c..918c5394 100644 --- a/content/docs/guides/troubleshooting.mdx +++ b/content/docs/guides/troubleshooting.mdx @@ -262,7 +262,7 @@ A working copy is a file that one build of the core made, and a build with a dif The message says how many writes the file holds that the server has not taken: waiting, blocked, refused or dead. Only the build that made the file can send or show them, so what you do depends on that count: -- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node or Swift binding can call `replaceRefused` instead: it sets the old file aside beside it and opens an empty copy, which holds none of what only the old file held, such as pins. +- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node or Swift binding can call `replaceRefused` instead: it replaces the old file with an empty copy, so what only the old file held, such as pins, is lost. - **Some.** Open the file with the build that made it, run `marfa device drain` to send the writes, and use `marfa device discard` on any it refuses that you don't want. Then hydrate a new copy with the newer build. Keep the old file until you have done this, because deleting it loses those writes. If some of the writes are of a kind the current build no longer queues, the message names the kind and how many, such as `1 write of kind merge_metadata`. That one is a tag merge, which an instance at v0.0.24 or later answers `404 not_found`. Read the tags it carries with `marfa device queue`, and add them with `marfa items tag`.