diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index 5eb661ad..4a973cc9 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -7,6 +7,24 @@ Each release of Marfa ships the server and its container image, the CLI for macO **Until the first public release, Marfa does not upgrade a database in place.** A build refuses a database whose schema differs from its own or that holds retired tables or settings, and leaves the file unchanged. If the build refuses your database, [Upgrade](/guides/upgrade) covers exporting with the old build and restoring into fresh storage. A version change alone does not cause a refusal. +## v0.0.19 + +Released October 21, 2026. [v0.0.19 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.19) + +For running an instance: + +- **The owner has General and Health pages.** General, at `/auth/owner/general`, shows the instance's name and the five retentions that change while the server runs, and saves only the ones you changed. Health, at `/auth/owner/health`, shows whether the database, saving changes, disk and file storage are working, and the object store when one is attached. It lists every housekeeping job with when it last ran, its error if it failed, and Run now. [Change settings while it runs](/guides/settings#change-settings-while-it-runs) and [Housekeeping](/guides/housekeeping) cover them. +- **The owner has Connected apps and Browsers pages.** Connected apps, at `/auth/owner/apps`, lists the apps you approved, what each can reach, and the keys it made. Disconnecting an app ends its sign-in at once, and its keys keep working unless you also revoke them there. Browsers, at `/auth/owner/browsers`, lists where you are signed in, and signs out one other browser or all of them. Disconnecting an app and signing out another browser need a sign-in within the last five minutes, and the page asks you to confirm it's you if yours is older. [Revoke access](/guides/authenticate#revoke-access) covers apps, and [Sign in as the owner](/guides/authenticate#sign-in-as-the-owner) covers browsers. +- **The owner has Activity and All settings pages.** Activity, at `/auth/owner/activity`, lists the [audit log](/guides/audit-log) newest first and filters it by kind, time and text. All settings, at `/auth/owner/settings`, lists every environment setting by group with its variable, whether it can change while the server runs, and its value now. A setting that holds a secret shows only whether it is set. +- **Eight more settings count as secrets.** `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `ERROR_WEBHOOK_URL`, `MARFA_HEARTBEAT_URL`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` and `MARFA_POSTHOG_HOST` can carry a credential in an address, so no page shows their value and a startup message that names one no longer prints it. `MARFA_AUTH_BASE_URL` stays readable and the server now refuses one that has a user name, password or query string. [Authentication](https://github.com/withmarfa/marfa/blob/main/conformance/spec/authentication.md#authenticationconsole-settings-no-secret) holds the rule. +- **The owner has Export and restore and Connectors pages, and the Manage and Restore pages are gone.** Export and restore, at `/auth/owner/export`, downloads an archive of everything or of one type and the types under it, with or without file bytes, and restores one. Connectors, at `/auth/owner/connectors`, lists each connector as Failing, Problems, Not heard from for over a day, or Running, shows its recent runs, and clears its saved state or removes it after you confirm. `/auth/owner/manage` and `/auth/owner/restore` no longer exist, and nothing redirects from them to the new pages. [Export an archive](/guides/back-up-and-restore#export-an-archive) and [Restore an archive](/guides/back-up-and-restore#restore-an-archive) cover them. + +For apps and devices: + +- **`PATCH /config` changes only the settings it names.** A setting in the body is replaced, a setting set to `null` is removed so its default applies, and every other setting stays. `enforcement` is one setting, so a patch that names it replaces all three levers. It takes the same fields and refusals as `PUT /config`. `marfa config update` sends it, with `--body` or `--file`. [Update the configuration](/guides/settings#update-the-configuration) covers it. +- **`GET /audit` filters by `kind` and `q`.** `kind` is one of `access`, `data`, `settings` and `housekeeping`, and each entry is of exactly one kind. `q` keeps the entries that hold every word you give, in any order, ignoring ASCII case. Any other `kind` is refused `400 validation_error`. `marfa audit` doesn't take either yet. [Filter it](/guides/audit-log#filter-it) covers them. +- **`GET /export?format=archive&files=false` leaves out file bytes.** The archive carries no file, and its manifest names each file it would have carried under `missing_blobs`. A restore of it reports those files in `blobs_missing` when the instance doesn't hold them. `marfa export` has no option for it yet. [Leave out the files](/guides/back-up-and-restore#leave-out-the-files) covers it. + ## v0.0.18 Released October 20, 2026. [v0.0.18 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.18) diff --git a/content/docs/guides/audit-log.mdx b/content/docs/guides/audit-log.mdx index 954bac77..59011b5d 100644 --- a/content/docs/guides/audit-log.mdx +++ b/content/docs/guides/audit-log.mdx @@ -67,14 +67,18 @@ Authorization: Bearer Without `--json`, the CLI prints one line for each entry: its ID, action and time. +**The owner can also read the log in a browser.** The Activity page, at `/auth/owner/activity`, lists the entries newest first and filters them by kind, time and text. + ## Filter it -Five filters narrow the log, and you can combine them: +Seven filters narrow the log, and you can combine them: - `action` keeps the entries with this action. - `resource_type` keeps the entries about this kind of thing. - `resource_id` keeps the entries about the thing with this ID. - `created_after` and `created_before` keep the entries recorded between two times, not counting an entry at either time itself. +- `kind` keeps the entries of one kind. Each entry is of exactly one: `access` is sign-ins, keys, folders and the owner's profile, `data` is items, edges, types, files and restores, `settings` is the configuration, webhooks and connectors, and `housekeeping` is what Marfa does on its own. Any other value is refused `400 validation_error`. +- `q` keeps the entries that hold every word of its text, in any order and ignoring ASCII case, in their action, resource, key, address or details. To see what happened to one item, filter by its ID: @@ -92,6 +96,8 @@ Authorization: Bearer 01a12312-d8f0-7301-94a8-558a479db26c item.create 2026-10-09T23:50:00.178Z ``` +`marfa audit` takes the first five of these and not `kind` or `q`. [The audit kinds](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md#eventsaudit-kind) and [the search](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md#eventsaudit-search) hold the rules. + **There is no filter for `key_id`**, so to see what one key did, read the entries and compare their `key_id`. A page holds 50 entries unless `limit` asks for up to 200. When more match, the answer carries a `next_cursor`: send it back as `cursor` (or `--cursor`) for the next page, and stop when it's `null`. **Marfa refuses a query key it doesn't know** with `400 validation_error`, so a misspelled filter never reads as one that matched everything. diff --git a/content/docs/guides/authenticate.mdx b/content/docs/guides/authenticate.mdx index 5d923925..973799cf 100644 --- a/content/docs/guides/authenticate.mdx +++ b/content/docs/guides/authenticate.mdx @@ -36,7 +36,23 @@ A program that makes the same request, with no page to show a dialog, gets `403 ## Start from Home -**Open the instance's address in a browser where you are signed in as the owner, and Home answers.** It is headed by the instance's name and address, and it lists the owner's pages in groups. A page that needs a look appears under Needs attention. Home has a search that finds a page or a setting by name, which Cmd-K on a Mac or Ctrl-K elsewhere also opens, and a Sign out button. The older management page, at `/auth/owner/manage`, still holds what the newer pages don't: server health, housekeeping, signed-in browsers, connected apps, connectors, and a link to restoring an archive. Home is never cached, because it belongs to the owner. +**Open the instance's address in a browser where you are signed in as the owner, and Home answers.** It is headed by the instance's name and address, and it lists the owner's pages in groups. A page that needs a look appears under Needs attention. Home has a search that finds a page or a setting by name, which Cmd-K on a Mac or Ctrl-K elsewhere also opens, and a Sign out button. Home is never cached, because it belongs to the owner. + +Home lists these pages: + +| Group | Page | Address | What the owner does there | +| --- | --- | --- | --- | +| Access | API keys | `/auth/owner/keys` | Makes, narrows and revokes keys. [Manage keys](/guides/manage-keys#use-the-api-keys-page) covers it. | +| Access | Connected apps | `/auth/owner/apps` | Sees what each approved app can reach and the keys it made, and disconnects it. [Revoke access](#revoke-access) covers it. | +| Access | Browsers | `/auth/owner/browsers` | Sees where the owner is signed in, and signs out another browser or every other one. [Sign in as the owner](#sign-in-as-the-owner) covers it. | +| Your data | Export and restore | `/auth/owner/export` | Downloads an archive and restores one. [Back up and restore](/guides/back-up-and-restore) covers it. | +| Connections | Connectors | `/auth/owner/connectors` | Sees each connector's state and recent runs, clears its saved state or removes it. | +| This Marfa | General | `/auth/owner/general` | Changes the name and the retentions. [Settings](/guides/settings#change-settings-while-it-runs) covers it. | +| This Marfa | Health | `/auth/owner/health` | Sees whether Marfa's own checks pass, and runs a [housekeeping job](/guides/housekeeping). | +| This Marfa | Activity | `/auth/owner/activity` | Reads the [audit log](/guides/audit-log). | +| This Marfa | All settings | `/auth/owner/settings` | Reads every [environment setting](/guides/settings#every-environment-setting) and its current value. A secret shows only whether it is set. | +| You | Profile | `/auth/owner/profile` | Changes the owner's name and email. | +| You | Password and passkeys | `/auth/owner/passkeys` | Changes the password and manages passkeys. | **A browser that isn't signed in sees a page that says the instance is running**, with a Sign in link, at `/`. **An owner page sends it to sign in, and back.** Opening an owner page, one under `/auth/owner/`, without a session answers `302` to `/auth/sign-in` with the page you asked for as `return_to`, and signing in returns you there. A page address longer than 2,048 characters returns you to Home instead. On an instance with no owner yet, the redirect goes to `/setup`. A program gets no redirect: a request with an `Authorization` header, or one that doesn't ask for HTML, is answered as before, and `GET /` for a program is still the instance's description. @@ -329,7 +345,7 @@ Content-Type: application/x-www-form-urlencoded token=marfa_rt_…&token_type_hint=refresh_token&client_id= ``` -Marfa answers `200`, even for a token it doesn't hold. The owner can also disconnect an app in the browser, and a credential holding `grants.manage` lists the apps signed in with `GET /auth/grants` and ends one with `DELETE /auth/grants/{id}`. +Marfa answers `200`, even for a token it doesn't hold. The owner can also disconnect an app on the Connected apps page, which asks whether to revoke the keys the app made too, and a credential holding `grants.manage` lists the apps signed in with `GET /auth/grants` and ends one with `DELETE /auth/grants/{id}`. **Disconnecting an app from a browser needs a sign-in within the last five minutes.** A browser that signed in earlier is asked to [confirm it's the owner](#confirm-its-you) first, and a request without a page is refused `403 forbidden` and leaves the app working. A credential holding `grants.manage` is not affected by when the owner last signed in, and neither is the private local command. diff --git a/content/docs/guides/back-up-and-restore.mdx b/content/docs/guides/back-up-and-restore.mdx index 4b47e324..575d6ee2 100644 --- a/content/docs/guides/back-up-and-restore.mdx +++ b/content/docs/guides/back-up-and-restore.mdx @@ -50,6 +50,8 @@ The [deployment guide](https://github.com/withmarfa/marfa/blob/main/deploy/READM **An archive holds what its caller can read.** A live owner browser session can export without a key or recent reauthentication. For automated exports with the CLI, make a key that reads everything and use it only for exports. The steps below use that key. +**The owner can also export in a browser.** Export and restore, at `/auth/owner/export`, downloads an archive of everything, or of one type and the types under it, and an Include files switch leaves the files out. + @@ -140,6 +142,17 @@ A file item can name bytes the instance has no record of, as after a restore tha A store that fails, or doesn't answer within 15 seconds, is not a lost file. The export, or a restore, is refused with `503 blob_store_unavailable` before anything is written, unless another store holds the blob whole. **Try again once the store answers.** +### Leave out the files + +**An archive without file bytes is much smaller than one with them.** Add `files=false` to the export, or turn off Include files on the Export and restore page: + +```http +GET /export?format=archive&files=false +Authorization: Bearer +``` + +The archive then carries no file, and its manifest names each file it would have carried under `missing_blobs`. A restore of it reports in `blobs_missing` each of those files that the instance it restores into doesn't hold. `marfa export` has no option for it, and `format=ndjson` carries no file bytes either way. [Export without files](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#search-and-filtersexport-without-files) holds the rule. + ### What an export checks **An export isn't taken at one instant**: an item created while it runs may not be in it. Stop your apps first if the archive must hold every write. @@ -163,6 +176,8 @@ A [source filter](/guides/settings#what-the-enforcement-levers-do) leaves items marfa --socket /srv/marfa/data/control/marfa.sock restore marfa-archive.tar.gz ``` +**In a browser, restore from the Export and restore page**, at `/auth/owner/export`: choose the archive and select Restore. A sign-in older than five minutes is asked to confirm it's the owner before the upload starts, and the page reports how many items, links, files and types it added. + The answer counts what the restore did. Into a fresh instance: ```json diff --git a/content/docs/guides/housekeeping.mdx b/content/docs/guides/housekeeping.mdx index 65e2355a..4345aa6e 100644 --- a/content/docs/guides/housekeeping.mdx +++ b/content/docs/guides/housekeeping.mdx @@ -5,7 +5,7 @@ description: See the work an instance does on itself, read what each housekeepin An instance looks after itself on a schedule: it purges the bin, thins old versions, moves expired feed items to the bin, clears old log entries, sends webhook deliveries, and copies and checks blobs. Each piece of that work is a housekeeping job, and one scheduler runs them all from one table. Reading what each job last did is how you check that an instance is keeping up, and running one yourself applies a new retention without waiting for its schedule. -**Listing needs the `instance.read` permission, and running needs `instance.maintain`.** These permissions apply to housekeeping operations. Item routes use their own permissions. The owner needs no key for either, through the private socket or signed in to the instance's Manage Marfa pages. +**Listing needs the `instance.read` permission, and running needs `instance.maintain`.** These permissions apply to housekeeping operations. Item routes use their own permissions. The owner needs no key for either, through the private socket or signed in to the Health page, at `/auth/owner/health`, which lists every job with when it last ran and its error if it failed, and runs one with Run now. ## List the housekeeping jobs diff --git a/content/docs/guides/settings.mdx b/content/docs/guides/settings.mdx index 2ecc717e..048e8d02 100644 --- a/content/docs/guides/settings.mdx +++ b/content/docs/guides/settings.mdx @@ -25,7 +25,7 @@ The server cannot start; fix these settings: TRASH_RETENTION_DAYS must be a whole number, from 0 to 36500 (got "forever") ``` -A setting that holds a secret is named, and its value is never printed. +A setting that holds a secret is named, and its value is never printed. A setting counts as one when it holds a credential or an address that can carry one, such as `S3_ENDPOINT` or `ERROR_WEBHOOK_URL`. ## The settings to check first @@ -46,6 +46,8 @@ Most settings have defaults that suit any instance. Check these before you keep The list comes from the server's own definition of its settings. The configurable intervals and retention windows of the [housekeeping jobs](/guides/housekeeping) are here too. +The owner can read the same settings in a browser, in groups with each one's current value, on the All settings page at `/auth/owner/settings`. **A setting that holds a secret shows only whether it is set**, on that page and in Home's search. [Console settings hold no secret](https://github.com/withmarfa/marfa/blob/main/conformance/spec/authentication.md#authenticationconsole-settings-no-secret) holds the rule. + src/generated/settings.md @@ -54,7 +56,7 @@ The list comes from the server's own definition of its settings. The configurabl ## Change settings while it runs -The configuration at `/config` **takes effect at once, with no restart**. Reading or replacing it needs the `config.manage` permission; the owner, signed in or through the private socket, needs no key. It holds these fields, and a field you haven't set is left out of the answer: +The configuration at `/config` **takes effect at once, with no restart**. Reading, replacing or updating it needs the `config.manage` permission; the owner, signed in or through the private socket, needs no key. It holds these fields, and a field you haven't set is left out of the answer: @@ -74,6 +76,8 @@ The configuration at `/config` **takes effect at once, with no restart**. Readin **A retention of `0` keeps those records with no age limit.** A retention you leave unset takes the environment setting's value. +**The owner can also change the name and the five retentions in a browser.** The General page, at `/auth/owner/general`, sends only the settings you changed. + The display name identifies the instance to apps and on its existing pages. **A name change keeps the same instance ID** and takes effect on subsequent requests. Marfa trims whitespace from each end and accepts up to 200 Unicode code points, with no control or bidirectional formatting characters. Leaving `name` out of a replacement restores `Marfa`. ### Read the configuration @@ -119,6 +123,29 @@ The answer is the configuration as it now stands. `marfa config replace --file c The server refuses a body with a field it doesn't know, an invalid name or a retention out of range, and keeps the configuration as it was. Each accepted replace is recorded in the [audit log](/guides/audit-log) as `config.update`. [Replace the configuration](/api-reference/instance/replaceConfig) has every refusal. +### Update the configuration + +**An update changes only the settings it names.** A setting in the body is replaced, a setting set to `null` is removed so that its default applies, and every other setting stays as it was. Two callers who each change one setting don't undo each other, as they can with a replace. This keeps the bin for 30 days and puts the name back to `Marfa`, and leaves the levers and the other retentions as they were: + +```bash tab="CLI" tab-group="client" +marfa config update --body '{"trash_retention_days": 30, "name": null}' +``` + +```http tab="HTTP" +PATCH /config +Authorization: Bearer +Content-Type: application/json + +{ + "trash_retention_days": 30, + "name": null +} +``` + +The answer is the whole configuration as it now stands. `--file config.json` reads the body from a file, and `--file -` from standard input. **`enforcement` is one setting**, so an update that names it replaces all three levers: include every lever you want on, and `null` removes them all. + +The server refuses an update with a field it doesn't know, an invalid name, a retention out of range or an `instance_id` other than the instance's own, and keeps the configuration as it was. Each accepted update is recorded in the audit log as `config.update`. [Update the configuration](/api-reference/instance/updateConfig) has every refusal, and [`PATCH /config` merges](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance.md#instanceconfig-patch-merges) holds the rule. + ## What the enforcement levers do Each lever names types. `strict_mode` and `source_allowlist` hold for exactly those types, **not the types that inherit from them**, so list each type you mean. diff --git a/openapi.json b/openapi.json index 15d78ac9..87107022 100644 --- a/openapi.json +++ b/openapi.json @@ -10888,7 +10888,7 @@ } }, "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `edge_constraint_violation`: an edge type on the item has `cascade_on_delete: block`, and such an edge exists.\n- `validation_error`: the item is a live `system.connection` (revoke its grant with `DELETE /auth/grants/{id}` first), or `version` is not a positive whole number.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `edge_constraint_violation`: an edge type on the item has `cascade_on_delete: block`, and such an edge exists.\n- `validation_error`: the item is a live `system.connection` (end it with `DELETE /owner/sign-ins/{id}` first), or `version` is not a positive whole number.", "content": { "application/json": { "schema": { @@ -14233,7 +14233,7 @@ } }, "400": { - "description": "- `invalid_id`: the ID is not a valid item ID.\n- `invalid_transition`: the item is not in the trash.\n- `validation_error`: the item is a live `system.connection` (revoke its grant with `DELETE /auth/grants/{id}` first), or `version` is not a positive whole number.", + "description": "- `invalid_id`: the ID is not a valid item ID.\n- `invalid_transition`: the item is not in the trash.\n- `validation_error`: the item is a live `system.connection` (end it with `DELETE /owner/sign-ins/{id}` first), or `version` is not a positive whole number.", "content": { "application/json": { "schema": { @@ -41434,7 +41434,7 @@ "Instance" ], "summary": "Replace the configuration", - "description": "Replaces the instance configuration with the body and returns it. A setting you leave out goes back to its default, so send the whole configuration with your change. Requires `config.manage`.", + "description": "Replaces the instance configuration with the body and returns it. A setting you leave out goes back to its default, so send the whole configuration with your change. To change only some settings, use `PATCH`. Requires `config.manage`.", "security": [ { "bearerAuth": [] @@ -41836,6 +41836,436 @@ "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." } ] + }, + "patch": { + "operationId": "updateConfig", + "tags": [ + "Instance" + ], + "summary": "Update the configuration", + "description": "Changes only the settings the body names and returns the whole configuration. A setting you leave out stays as it is, and `null` removes one so its default applies. `enforcement` is one setting: send every lever you want on. Requires `config.manage`.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "instance_id": { + "type": "string", + "minLength": 1, + "description": "This instance's ID, accepted so you can send back what `GET /config` returned. It sets nothing." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 200, + "description": "The instance's display name. `null` removes it, so the server's default applies." + }, + "enforcement": { + "type": [ + "object", + "null" + ], + "properties": { + "strict_mode": { + "allOf": [ + { + "$ref": "#/components/schemas/TypeLeverStrict" + }, + { + "description": "Types on which a write may not set a property the type doesn't declare." + } + ] + }, + "source_allowlist": { + "allOf": [ + { + "$ref": "#/components/schemas/TypeAndSourceLeverStrict" + }, + { + "description": "Types that take new items only from the listed sources." + } + ] + }, + "source_filter": { + "allOf": [ + { + "$ref": "#/components/schemas/TypeAndSourceLeverStrict" + }, + { + "description": "Types whose listings, searches, exports, event streams and webhook deliveries show only items from the listed sources. A read by ID isn't narrowed, and a copy stream sends such items with `listed: false`." + } + ] + } + }, + "additionalProperties": false, + "description": "The instance's enforcement levers. Sending this replaces all three levers, so include every lever you want on. `null` removes them all." + }, + "audit_retention_days": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 36500, + "description": "Days Marfa keeps audit log entries and outbound webhook delivery history. `0` keeps them with no age limit. `null` removes it, so the server's default applies." + }, + "event_log_retention_hours": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 876000, + "description": "Hours Marfa keeps events, which a stream can replay from a cursor, and the answers it replays for an `Idempotency-Key`. `0` keeps them with no age limit. `null` removes it, so the server's default applies." + }, + "trash_retention_days": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 36500, + "description": "Days an item stays in the trash before Marfa purges it. `0` keeps them with no age limit. `null` removes it, so the server's default applies." + }, + "inbound_handled_retention_days": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 36500, + "description": "Days Marfa keeps inbound webhook deliveries a connector has marked handled. `0` keeps them with no age limit. `null` removes it, so the server's default applies." + }, + "inbound_pending_retention_days": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 36500, + "description": "Days Marfa keeps inbound webhook deliveries no connector has marked handled. `0` keeps them with no age limit. `null` removes it, so the server's default applies." + } + }, + "additionalProperties": false + } + } + } + }, + "responses": { + "200": { + "description": "Returns the configuration as stored.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceConfig" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "- `missing_required_field`: a lever lacks `types` or `sources`.\n- `validation_error`: the body names a setting Marfa doesn't know, a retention value is out of range, the name is invalid, or `instance_id` names a different instance.", + "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`: you don't hold `config.manage`. `details.required_scope` names it.", + "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" + } + } + }, + "409": { + "description": "`instance_changed`: `X-Marfa-Instance-Id` names another instance. Nothing is written. Rebuild the working copy from the instance at this address.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceChangedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "413": { + "description": "`request_too_large`: the request body is larger than this instance accepts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestTooLargeRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "429": { + "description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitedRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + }, + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + }, + "500": { + "description": "`internal_error`: Marfa failed in a way it didn't expect, and the request may not have completed. Read what you changed before you repeat a write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalErrorRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "503": { + "description": "`write_contention`: the database was busy, and Marfa couldn't complete the request in time. Nothing changed. Try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WriteContentionRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + }, + "507": { + "description": "`insufficient_storage`: the disk that holds the instance's data has no room for the request, or the request would leave less free than the instance keeps in reserve. Nothing changed, unless `details.write_outcome` is `unknown`, which means the write may have landed: read what you changed before you repeat it. Free space on the disk, then try the request again.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientStorageRefusal" + } + } + }, + "headers": { + "X-Marfa-Contract": { + "$ref": "#/components/headers/X-Marfa-Contract" + }, + "X-Request-ID": { + "$ref": "#/components/headers/X-Request-ID" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-Error-Code": { + "$ref": "#/components/headers/X-Error-Code" + } + } + } + }, + "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." + } + ] } }, "/restore": { @@ -46265,12 +46695,26 @@ "ndjson", "archive" ], - "description": "`ndjson` for lines of JSON, or `archive` for a `.tar.gz` file that `POST /restore` reads. Defaults to `ndjson`. An archive's `manifest.json` lists in `missing_blobs` each blob no store could read, which the archive doesn't hold." + "description": "`ndjson` for lines of JSON, or `archive` for a `.tar.gz` file that `POST /restore` reads. Defaults to `ndjson`. An archive's `manifest.json` lists under `missing_blobs` each blob it doesn't hold." }, "required": false, - "description": "`ndjson` for lines of JSON, or `archive` for a `.tar.gz` file that `POST /restore` reads. Defaults to `ndjson`. An archive's `manifest.json` lists in `missing_blobs` each blob no store could read, which the archive doesn't hold.", + "description": "`ndjson` for lines of JSON, or `archive` for a `.tar.gz` file that `POST /restore` reads. Defaults to `ndjson`. An archive's `manifest.json` lists under `missing_blobs` each blob it doesn't hold.", "name": "format", "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ], + "description": "With `format=archive`, `false` leaves out the bytes of files, which the manifest then names in `missing_blobs`. Defaults to `true`. `format=ndjson` carries no file bytes either way." + }, + "required": false, + "description": "With `format=archive`, `false` leaves out the bytes of files, which the manifest then names in `missing_blobs`. Defaults to `true`. `format=ndjson` carries no file bytes either way.", + "name": "files", + "in": "query" } ], "responses": { @@ -49778,6 +50222,33 @@ "name": "resource_id", "in": "query" }, + { + "schema": { + "type": "string", + "enum": [ + "access", + "data", + "settings", + "housekeeping" + ], + "description": "Only return entries of this kind: `access` is sign-ins, keys, folders and the profile, `data` is items, edges, types, files and restores, `settings` is configuration, webhooks and connectors, and `housekeeping` is what Marfa does on its own." + }, + "required": false, + "description": "Only return entries of this kind: `access` is sign-ins, keys, folders and the profile, `data` is items, edges, types, files and restores, `settings` is configuration, webhooks and connectors, and `housekeeping` is what Marfa does on its own.", + "name": "kind", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 200, + "description": "Only return entries that hold every word of this text, in any order and ignoring ASCII case, in their action, resource, key, address or details." + }, + "required": false, + "description": "Only return entries that hold every word of this text, in any order and ignoring ASCII case, in their action, resource, key, address or details.", + "name": "q", + "in": "query" + }, { "schema": { "type": "string", diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index 755e9375..9519e38d 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.18/marfa-0.0.18-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.19/marfa-0.0.19-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 017ece03..6c49831e 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -31,12 +31,14 @@ "authentication/confirm-passkey-owner": "874f470a6620c4e4", "authentication/confirm-needs-session": "ce8ac0c02906f879", "authentication/confirm-origin": "248cf3e06973c5d4", + "authentication/sign-out-others": "b1fc82bcd7dad72a", "authentication/passkey-register-recent": "867c32e933ffddb8", "authentication/console-sign-in": "33df0f8ae12e0858", "authentication/console-sign-in-no-store": "6557bafa3e9a287c", "authentication/console-no-redirect": "379183a0c6f866ae", "authentication/console-unclaimed": "e38dd34fd5163837", - "authentication/console-keys-no-plaintext": "e12c41e4070f02cc" + "authentication/console-keys-no-plaintext": "e12c41e4070f02cc", + "authentication/console-settings-no-secret": "96bd41e766474f75" }, "blobs": { "blobs/read-idle-lifetime": "343c013f840aa398", @@ -741,6 +743,9 @@ "device/wrong-schema-says": "2d5673c280a0de5c", "device/wrong-schema-no-delete": "3cb3cfa4cc6ef74c", "device/writer-per-file": "623e1989deb95557", + "device/writer-lock-unwritable": "12f35aa7c2c2f21e", + "device/command-writer-lock-unwritable": "fdd393377cd321cd", + "device/reader-lock-unwritable": "87e356e594c05236", "device/command-makes-store": "82a06554180efe52", "device/command-no-store": "4f48ed2cdd209b77", "device/owner-only": "140b3deff0b4f439", @@ -1612,6 +1617,9 @@ "events/audit-export-refused": "2e8df4b82220c3ae", "events/audit-export-contention": "c3e966c4dc9981a7", "events/audit-filter": "6c97f9a3ec3bad6e", + "events/audit-kind": "e0e08db4ea1c466b", + "events/audit-kind-invalid": "88dcb3e019946afa", + "events/audit-search": "92d2d1a291f73131", "events/audit-bounds": "2e91e3533248be5a", "events/audit-bound-precision": "d150ac7e67a5e858", "events/audit-bound-invalid": "bc4b68039c3432f8", @@ -2346,6 +2354,9 @@ "folders/watch-record-written-again": "ce27bb01cb08e7a5", "folders/handover-asked": "40629ac690e1bf01", "folders/handover-wait-refused": "3e28e75d781e0744", + "folders/writer-lock-unwritable": "76b931cd4fd45bda", + "folders/status-lock-unwritable": "9550e1f24f7359a0", + "folders/handover-request-unwritable": "be433c3b9bebc4cf", "folders/handover-step-whole": "a9b4319775b2b333", "folders/handover-others-go-on": "0feb23f2bd2b3f5d", "folders/handover-taken-back": "52fb933ebf41d1d8", @@ -3006,6 +3017,7 @@ "instance/root-home": "50bdb68a4574ccb5", "instance/root-home-search": "01113994fb342a31", "instance/root-home-no-store": "07150c564aec3a48", + "instance/settings-owner-read": "37abd861c5e61c61", "instance/root-search-key": "f7d68a6991981226", "instance/root-search-key-other": "c9c097224350f3ad", "instance/page-policy": "e1a892a64626051b", @@ -3037,6 +3049,9 @@ "instance/config-read-set": "beef2bbd768c1927", "instance/config-read-unset-absent": "8e6818944c76fe81", "instance/config-omitted-removed": "32d08333c26c067e", + "instance/config-patch-merges": "660577eb3c5a6c05", + "instance/config-patch-clears": "c2d3a697d0e2faf6", + "instance/config-patch-refused": "9250b9eb60c0daa8", "instance/config-audit": "75d926bfaf9fd790", "instance/config-refused-no-audit": "a75858588f503016", "instance/config-query-key": "3e852ff0cd143f00", @@ -3071,7 +3086,7 @@ "instance/settings-refused-names-each": "d89e4886167da333", "instance/settings-refused-only-bad": "6c7220d47ff205fd", "instance/settings-refused-value-shown": "2bf8dd09f3d97b00", - "instance/settings-refused-secret-hidden": "b8d6c8720e720b6f", + "instance/settings-refused-secret-hidden": "929e295748daefd1", "instance/settings-secret-padded": "6e9568a20c24016d", "instance/settings-auth-secret-short": "d67cd95b3fa9e368", "instance/settings-cross-field": "29a3c57733636619", @@ -4463,6 +4478,7 @@ "search-and-filters/export-type-map": "a7a216d741039b96", "search-and-filters/export-extensions": "66fa1a0f3652f50c", "search-and-filters/export-format": "95393e754b605558", + "search-and-filters/export-without-files": "51b41e9cbab5a39c", "search-and-filters/export-state-default": "d2533e347aa670b8", "search-and-filters/export-state-any": "e86a59bb0840d5a9", "search-and-filters/export-state-trashed": "dbf286ab487be108", @@ -4809,7 +4825,7 @@ "types/config": "5964ff2161348659", "types/config-refused": "693caebb7982166e", "types/config-lever-missing-field": "a212d6854dafb6a7", - "types/config-permission": "758346523651eb98", + "types/config-permission": "1664585ec7c78a8e", "types/strict-mode": "ced46919637d67bd", "types/strict-mode-code": "2d8160282150414c", "types/strict-mode-declared": "dfd50e8c44aa1e22", diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index 449e391d..664a6e99 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.18"} +{"version":"v0.0.19"} diff --git a/src/generated/server-image.md b/src/generated/server-image.md index 4f79b126..7515dab5 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.18 +docker pull ghcr.io/withmarfa/marfa-server:0.0.19 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.18 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.19 ``` diff --git a/src/generated/settings.md b/src/generated/settings.md index c38ba236..96ebedb0 100644 --- a/src/generated/settings.md +++ b/src/generated/settings.md @@ -47,7 +47,7 @@ | `TRUSTED_PROXY_HEADER` | None | An HTTP header name | The name of a header that the platform's edge overwrites with the client address. It takes precedence over `TRUSTED_PROXY_CIDRS`. | | `API_KEY_SALT` | None. Required in production. | Text with no whitespace around it | The salt for hashing API keys and app tokens. Changing it invalidates every existing key and token. Outside production, an unset salt falls back to a built-in one. | | `MARFA_AUTH_SECRET` | None. Required in production. | Text of at least 32 characters, with no whitespace around it | The secret that signs sign-in cookies, authorize queries, read-view tokens and credential-free blob links. It must be at least 32 characters. Outside production, an unset secret falls back to a random one for each process. | -| `MARFA_AUTH_BASE_URL` | None. Required in production. | Absolute `http` or `https` URL | The public URL for OAuth, cookies, and links. Outside production, an unset URL uses the assigned port with `http://127.0.0.1` when `MARFA_LISTEN_HOST` is `127.0.0.1`, otherwise `http://localhost`. | +| `MARFA_AUTH_BASE_URL` | None. Required in production. | Absolute `http` or `https` URL | The public URL for OAuth, cookies, and links. It has no user name, password or query string. Outside production, an unset URL uses the assigned port with `http://127.0.0.1` when `MARFA_LISTEN_HOST` is `127.0.0.1`, otherwise `http://localhost`. | | `CORS_ORIGINS` | None | Comma-separated origins, each `scheme://host[:port]` with no path | The browser origins allowed to call the API across origins, beside the web pages the owner approves, and trusted by the sign-in forms. Each is an exact scheme and host with no trailing slash. | | `MARFA_PERMISSION_BUNDLES` | None | JSON array of permission bundles | The permission bundles the consent screen offers, replacing the built-in ones. Each entry has a string `id`, an array of `scopes` and a boolean `default_on`. | | `ENABLE_HSTS` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server sends the `Strict-Transport-Security` header. |