diff --git a/content/docs/(overview)/access.mdx b/content/docs/(overview)/access.mdx index b5978511..56b5ed38 100644 --- a/content/docs/(overview)/access.mdx +++ b/content/docs/(overview)/access.mdx @@ -55,6 +55,6 @@ Every key has a source of its own, chosen when it's made, such as `laptop` or `t **You make a key** as the owner, or with a key that holds the `keys.mint` permission, and say what it holds. A key made by another key can hold only the permissions and maps its maker holds, and a key can be set to expire. **The maker's `enforcement_override` isn't carried over**: unless the new key names one, it follows the instance's levers ([Settings](/guides/settings) describes them), so it can list items that its maker's levers hide. [Manage keys](/guides/manage-keys) shows how to make, narrow and revoke keys. -**An app is never given a key.** It registers itself with the instance and sends the owner to approve it, as when you sign in to an app with your Google account. The owner sees each scope the app asks for, such as `core.note:read`, and the app's token holds only what the owner leaves ticked. A browser that signed in more than five minutes ago can approve reading only; write access and permissions need a recent sign-in. The approval stands until the owner revokes it, or until the app goes unused for a year. [Authenticate](/guides/authenticate) shows an app's side of it. +**An app is never given a key.** It registers itself with the instance and sends the owner to approve it, as when you sign in to an app with your Google account. The owner chooses what to allow for each item type the app asks for, such as `core.note:read`, at the level No access, Read only or Read and edit, and the app's token holds only what the owner chooses. An app that asks for all data can be given chosen types instead. Approving a write or a permission that the app doesn't already hold needs a sign-in from the last five minutes, which the page asks the owner to confirm. The approval stands until the owner revokes it, or until the app goes unused for a year. [Authenticate](/guides/authenticate) shows an app's side of it. The rules behind this page are in the [keys and sign-in](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md), [authentication](https://github.com/withmarfa/marfa/blob/main/conformance/spec/authentication.md), [owner counts and exports](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#search-and-filtersowner-count-export), and [instance claim](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance-claim.md) chapters of the contract. diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index 4eb5718c..728e18b9 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -41,11 +41,19 @@ Each write the instance answers gets one verdict: | `merged` | Another change had touched the same field, and the instance settled the two by the type's rule, such as keeping the later title. | | `conflicted` | The two changes collided, and the instance kept both: the item keeps its value, and the write's value goes to a conflicted copy. | | `refused` | The instance refused the write, for example because it broke the item's type. The device keeps its content so an app can offer it back. | -| `blocked` | Something has to change before the write can go, such as an expired credential. | +| `blocked` | Something has to change before the write can go, such as an expired credential, or the write keeps meeting a failure that other writes don't. | | `dead` | The write failed five times in ways that count against it, such as an answer the device couldn't read. A lost connection or a server error never counts. | **Only `refused`, `blocked` and `dead` need an app's attention.** An app decides what to do with each: send it again, withdraw it, or let the person decide. [Versions](/versions) explains how the instance settles two changes to one item. +A `blocked` write has one of six reasons: `credential_refused`, `key_spent`, `ancestor_unavailable`, `conflict_unresolved`, `awaiting_dependency` and `failing_alone`. + +**A device sets a write aside as `failing_alone` when it keeps failing in the words of an outage while the server still answers other writes.** A gateway can refuse one write in those words, such as an HTML `413` page for a large upload, and that write would otherwise end every drain and hold back the writes behind it. After the write has failed in several drains over some minutes, the device tests it: it sends another write that doesn't depend on it and, if the server answers in a way that lets the device go on, sends the failing write once more. If that fails in the same way, the device blocks the write as `failing_alone` and carries on with the rest of the queue, and the writes that depend on it wait as `awaiting_dependency`. The exact rules are in the contract: [`queue-and-verdicts/stuck-write-suspect`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstuck-write-suspect) for when a write becomes a suspect, and [`queue-and-verdicts/stuck-write-test-refused`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstuck-write-test-refused) for an answer that ends the test. + +Releasing the write gives it a fresh try, and withdrawing it refuses the writes that wait on it. + +The queue shows the last answer a write met in this way. `marfa device queue` prints it after `last answer:`, as `HTTP ` with the start of the body when the body is text, or `no answer`. + ## Folders A device's working copy can also be a directory of files on your computer: **a folder**. Each item the folder holds is a file, a note becomes a Markdown file, and an edit to either the file or the item reaches the other. diff --git a/content/docs/(overview)/properties-metadata-and-edges.mdx b/content/docs/(overview)/properties-metadata-and-edges.mdx index 65af7154..9bd1a92a 100644 --- a/content/docs/(overview)/properties-metadata-and-edges.mdx +++ b/content/docs/(overview)/properties-metadata-and-edges.mdx @@ -51,6 +51,8 @@ Content-Type: application/json { "tags": ["favorite"] } ``` +**A tag can't be empty, blank or longer than 128 UTF-16 code units, and it can't be `.` or `..`.** Marfa refuses such a tag `400 validation_error` on every write that carries tags except a restore, and for `.` or `..` the message in `details.errors[]` names the tag. A path can't name `.` or `..`, because a URL parser drops them, so such a tag could never be removed with `DELETE /items/{id}/tags/{tag}`. + An extension is a JSON object stored on an item under a namespace, such as `com.example.reader`. Whoever writes a namespace decides what goes in it, and **one app's extension stays out of another's way**: a key reads and writes only the namespaces it was given. ```http diff --git a/content/docs/(overview)/quickstart.mdx b/content/docs/(overview)/quickstart.mdx index b143802e..1f97d966 100644 --- a/content/docs/(overview)/quickstart.mdx +++ b/content/docs/(overview)/quickstart.mdx @@ -5,7 +5,7 @@ description: Start Marfa on your own computer, claim it, make a key, and write a By the end of this page you'll have a Marfa instance running on your computer, with you as its owner and one note in it. You need two terminal windows: one for the server, one for everything else. This instance is for trying Marfa; [Deploy](/guides/deploy) covers one you keep. -**You need** macOS or Linux, with Git, [Node.js](https://nodejs.org) 22.12 or later and [pnpm](https://pnpm.io/installation). On Linux, also install [Rust](https://www.rust-lang.org/tools/install), to build the CLI, and the `acl` package, which the server uses to check who may reach its private socket. +**You need** macOS or Linux, with Git, [Node.js](https://nodejs.org) 22.14 or later and [pnpm](https://pnpm.io/installation). On Linux, also install [Rust](https://www.rust-lang.org/tools/install), to build the CLI, and the `acl` package, which the server uses to check who may reach its private socket. @@ -74,7 +74,7 @@ The CLI asks for a password for the owner, then for the setup code the server pr -Open `http://localhost:8600/setup`, enter the setup code the server printed, and choose the owner's email and password. **Claiming signs you in**, and setup goes on from there to add a passkey, name the instance, connect something and read how it's backed up. +Open `http://localhost:8600/setup`, enter the setup code the server printed, and choose the owner's email and password. **Claiming signs you in**, and setup goes on from there to add a passkey, name the instance, connect something and read how it's backed up. Done opens [Home](/guides/authenticate#start-from-home). diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index 01a8b0c2..5eb661ad 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -7,6 +7,38 @@ 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.18 + +Released October 20, 2026. [v0.0.18 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.18) + +For running an instance: + +- **The owner has a Home page at `/`.** A browser signed in as the owner sees the instance's name and address, what needs attention, a search for pages, and Sign out. A browser that isn't signed in gets a page that says the instance is running, with a Sign in link. An owner page under `/auth/owner/` sends a browser with no session to the sign-in page and returns it to the page it asked for, and an unclaimed instance sends it to `/setup`. A program that requests `/` still gets the description of the instance. [Start from Home](/guides/authenticate#start-from-home) covers it. +- **The owner manages API keys in the browser.** The API keys pages list the keys, create one, and edit or revoke one. A key's access starts off, and the secret shows once and is removed when the owner leaves the page. A save is refused if the key changed after the page opened. [Use the API keys page](/guides/manage-keys#use-the-api-keys-page) covers it. +- **The owner has Profile and Password and passkeys pages.** Adding, renaming or removing a passkey asks the owner to confirm it's them when the sign-in is more than five minutes old. Changing the password asks for the current password instead. A passkey name that is empty or too long is refused `400 validation_error` when the passkey is verified. [Authenticate](/guides/authenticate#sign-in-as-the-owner) covers it. +- **A folder that a different directory replaces stays listed.** A folder registry entry records the directory's identity. When another directory takes the folder's place, the entry stays, and the watch resumes when the directory the folder was admitted at returns. Marfa removes an entry when the directory at its path holds no folder state and isn't known to differ from the one admitted. [Sync a folder](/guides/sync-a-folder#a-missing-or-unreadable-directory) covers it. +- **Sign out stays beside the title on a phone.** On Home, the Sign out button no longer drops under the title and address. +- **Destructive actions sit in their own card, and Save appears only after a change.** Revoking a key and removing a passkey are in a bordered card of their own. A key's page shows Save and Discard, with a count of the changes, only once something has changed. +- **The server needs Node.js 22.14 or later.** The container image runs Node.js 24. [Deploy](/guides/deploy) covers it. + +For apps and devices: + +- **The owner chooses what an app gets.** The approval screen lists each type with No access, Read only or Read and edit, then the owner's profile and the powers over Marfa that are off until the owner turns them on. Three powers that can't be undone are marked. Access the app already holds shows as Already allowed. A request for all data also shows Choose types, and if the owner picks types the token names them instead of all data. If the request asks for access that the app doesn't already hold and the owner turns all of it off, the app gets `access_denied` and keeps what it held. A request for nothing but `openid` and `offline_access` offers nothing to turn off, so the owner can approve it. [What the owner chooses](/guides/authenticate#what-the-owner-chooses) covers it. +- **A device code is easier to type.** The device page shows a code as `XXXX-XXXX`, accepts it in any case with or without the hyphen, and a code ends after 10 minutes. +- **Approving or revoking asks to confirm in a dialog, not a redirect.** `POST /auth/confirm` takes the owner's password or a passkey and replaces the session, so the browser carries on at the same page. A password counts against the attempt windows: a wrong one answers `401 unauthorized`, and once the windows are spent the answer is `429 rate_limited` with `Retry-After`. A refused confirmation keeps the session. A program that calls a route that needs a recent sign-in gets `403 forbidden` with "Sign in again to perform this operation". `POST /auth/confirm` answers `401` to a request with no live owner session or with a key. A browser may end its own sign-in at any time. [When approval needs a recent sign-in](/guides/authenticate#when-approval-needs-a-recent-sign-in) covers it. +- **`narrow_only` keeps a key from gaining access.** `PATCH /keys/{id}` with `narrow_only: true` refuses a change that adds access, and the API keys pages send it. `POST /keys` answers `Cache-Control: no-store`, so the secret isn't cached. [Narrow a key](/guides/manage-keys#narrow-a-key) covers it. +- **Marfa keeps a whole number exact, or refuses it.** A `number` or `integer` value that Marfa can't serve back with the same digits, such as `1500000000000000001`, is refused `400 invalid_properties`, and `details.errors` names the field. A `url` value is trimmed and judged as the WHATWG URL Standard does. For a special scheme such as `http`, a host with a non-ASCII character must be valid IDNA, and a host with a forbidden code point is refused. [Field kinds](/catalog/field-kinds) covers it. +- **A tag of `.` or `..` is refused** with `400 validation_error`, as an empty, blank or over-long tag is. A restore still accepts what an archive holds. +- **Narrowing a webhook cancels the deliveries it no longer selects.** Pending deliveries that the narrowed subscription doesn't select end `canceled`. Changing the `url` or setting `active: false` cancels all of them, and widening revives none. [Change what a webhook sends](/guides/receive-webhooks#change-what-a-webhook-sends) covers it. +- **The CLI prints access in the words the approval page uses.** `marfa login`, `whoami`, and `keys list`, `current`, `create` and `update` print No access, Read only and Read and edit for each type, and name powers by their titles. `--json` is unchanged. [CLI](/cli#check-what-a-command-will-use) covers it. +- **A write that fails alone is set aside.** A device marks a queued write `blocked` with the reason `failing_alone` when it keeps failing in the words of an outage, over some minutes, while the server still answers other writes. It tests the write first by sending another write that doesn't depend on it, and sets the write aside only if it fails in the same way when sent once more. Its dependents wait as `awaiting_dependency`. `marfa device queue` prints the last answer the write met after `last answer:`. [Devices](/devices#verdicts) covers it, and [`queue-and-verdicts/stuck-write-suspect`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstuck-write-suspect) has the exact rule. +- **A file item keeps its bytes at a document name.** A file already bound to a file item, at a name ending `.md`, `.markdown` or `.txt`, is sent as bytes whatever they hold, with no frontmatter read. If it is empty it waits, and the rest of the scan goes on. A file at such a name that isn't bound to a file item is still read as a document. A `marfa_id` over 256 bytes is no id. +- **`--no-keychain` keeps the CLI off the keychain.** `--no-keychain` or `MARFA_NO_KEYCHAIN=1` makes every command treat the keychain as absent, and `marfa login --print-token` no longer reads or writes it. A command that sends a key from `--key` or `MARFA_API_KEY` and has no server named is refused `no_server`. [CLI](/cli#run-without-the-keychain) covers it. +- **The approval rows of two types with the same label are told apart.** When two item types share a label, such as `core.task` and `aic.ticket.task`, each shows its identifier under the label, and the type search matches the identifier. +- **A lock file the CLI can't write is refused with a message.** When the lock file opens read-only, the CLI stops with ` cannot be opened for writing, so it cannot be locked: check that it is writable`. +- **A repeat of a write whose first answer was large is refused, not run again.** Marfa keeps only the status of a first answer over 1,048,576 bytes, so a repeat under the same `Idempotency-Key` answers `422 idempotency_result_not_retained` with `details.original_status`, and writes nothing. A device counts a `2xx` status there as accepted, except for a create that names no `id`, which it refuses. [Write safely](/guides/write-safely#retry-with-an-idempotency-key) covers it. +- **A waiting edge edit or delete stays in view through a catch-up.** When a catch-up brings an item into a device's copy along with its edges, the device lays its own waiting writes over those edges, as hydration does, instead of showing the server's version until the server answers. + ## v0.0.17 Released October 20, 2026. [v0.0.17 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.17) diff --git a/content/docs/(reference)/cli.mdx b/content/docs/(reference)/cli.mdx index 5d153543..9cef1e6d 100644 --- a/content/docs/(reference)/cli.mdx +++ b/content/docs/(reference)/cli.mdx @@ -41,7 +41,7 @@ These docs follow Marfa's `main` branch, which can be ahead of the latest releas A command that talks to an instance needs the server's address and a credential. **The CLI can keep both for you**, so a bare command such as `marfa status` knows where to go. There are two ways to give it a credential. -**Sign in as an app** with `marfa login`. The CLI shows a code and opens a page on the server, where you sign in as the owner and approve what it asks for. The CLI asks for more than reading, so the page asks you to sign in again if you signed in more than five minutes ago and haven't approved the CLI for those scopes before. It then keeps the app's token in the keychain: +**Sign in as an app** with `marfa login`. The CLI shows a code and opens a page on the server, where you sign in as the owner and approve what it asks for. If you turn on access beyond reading that the CLI doesn't already hold, and you signed in more than five minutes ago, the page asks you to confirm it's you when you approve. It then keeps the app's token in the keychain: ```bash marfa login --url https://marfa.example.com @@ -50,11 +50,20 @@ marfa login --url https://marfa.example.com To print the token set instead of keeping it, add `--print-token`. The command then doesn't read or write the keychain, so the server must come from `--url` or `MARFA_API_URL`. ```text -Open https://marfa.example.com/auth/device?user_code=V3Z5RBBC and enter the code V3Z5RBBC +Open https://marfa.example.com/auth/device?user_code=V3Z5RBBC and enter the code V3Z5-RBBC Waiting for the approval... ``` -The token holds only the scopes you approve. The CLI asks for everything an owner can grant except `instance.read`, `instance.maintain`, `connectors.manage`, `blobs.manage` and `keys.manage`, so the commands that need one of those need a key instead. `marfa logout` revokes the token and forgets it. +The token holds only the scopes you approve, and `marfa login` ends by printing them in the words the approval page uses: + +```text +signed in to https://marfa.example.com; the token is in the keychain + core.note Read and edit + core.task Read only + Powers: See and disconnect other apps +``` + +The CLI asks for the scopes in `OWNER_SCOPES` in [its source](https://github.com/withmarfa/marfa/blob/main/core/marfa-cli/src/auth.rs): all data, all edges, metadata, the permissions and the sign-in scopes. The page offers those, and you turn on the ones you want. `marfa logout` revokes the token and forgets it. Or **keep a key** you already have with `marfa keys keep`. It reads the key from `MARFA_API_KEY`, or, if that isn't set, from standard input, so you can paste it and press Return. It checks the key with the server, then keeps it in the keychain. @@ -121,6 +130,8 @@ a key from the keychain The instance name comes from its live configuration and defaults to Marfa. [Settings](/guides/settings#change-settings-while-it-runs) shows how to change it. After the name comes the **release** the server was built as, then the commit it was built from. `marfa status` names them the same way. A server built from a clone shows only the commit, because it has no release. +**Access is printed in the words the approval page uses.** Under an app's token, `marfa whoami` lists what the owner approved, and `marfa keys list`, `current`, `create` and `update` list what a key holds. Each item type shows as No access, Read only or Read and edit, named by its identifier, and each permission by its title, such as "See health, metrics, storage and job status". `--json` prints structured output. + When a command reaches the wrong instance, or is refused, start here. ## On the instance's own machine diff --git a/content/docs/(reference)/swift-sdk.mdx b/content/docs/(reference)/swift-sdk.mdx index ea725ae8..46cc2a0d 100644 --- a/content/docs/(reference)/swift-sdk.mdx +++ b/content/docs/(reference)/swift-sdk.mdx @@ -21,6 +21,8 @@ The SDK supports iOS 27 and macOS 27, and builds with Xcode 27 on a Mac with App Replace `` with the version of the [latest release](https://github.com/withmarfa/marfa-swift/releases/latest). Every release adds 0.0.1 to the version, whatever it changes, so pin one version and move to the next on purpose. +**An added enum case breaks an exhaustive switch** that has no default. A switch over `BlockedReason` must handle `.failingAlone` as well as the other cases, or provide a default. + Each release carries the core already built, so your app needs no Rust toolchain. The core is built for Apple silicon only, so in your app target's build settings, set Excluded Architectures for Any iOS Simulator SDK to `x86_64`. Without it, some simulator builds fail to link. ## Open a working copy @@ -86,6 +88,10 @@ _ = try await copy.items.update(id, Edit(properties: ["title": "Books to read"], Each write shows in the copy at once, with or without a connection, and waits in the queue. An edit changes the properties it names and keeps the rest. With `replaceProperties: true`, its properties become the item's whole set, so that's how you remove a property. +If Marfa would serve a whole number back with other digits, the write throws `MarfaError.validation` with code `invalid_properties` **before anything is queued**. This includes `.integer(9_007_199_254_740_993)`, even inside an array or object, in an edge's properties or in an extension's data. + +Send a value that must keep every digit as a string. [Field kinds](/catalog/field-kinds) gives the number rule. + ## Read the bin `copy.items.bin(type:after:limit:)` reads **the server's bin online**, independently of the copy's slice, and adds none of its items to the copy. Follow `nextCursor` even when a page is short or empty: @@ -180,6 +186,19 @@ updateItem accepted A refused write that carried content stays even then, so your app can offer the content back, until you call `queue.discard(_:)`. [Devices](/devices#verdicts) explains each verdict. +**`BlockedReason.failingAlone`** means the write keeps meeting a failure while the server answers other writes. It stays blocked until you release or withdraw it. Dependent writes have `waiting` set to `true`, with no verdict. + +The queue exposes what the write met: + +| Property | Meaning | +| --- | --- | +| `QueuedWrite.environmentalStreak` | The number of drains in a row in which the write met a failure that was no answer to it. [`queue-and-verdicts/streak-counted`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstreak-counted) and [`queue-and-verdicts/streak-run-restarts`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstreak-run-restarts) say what counts and when the count starts again. | +| `QueuedWrite.lastFailure` | An optional `LastFailure` describing the last such failure. A `429` updates it without increasing the streak. Failures in credential renewal or the device's own storage leave it unchanged. | +| `LastFailure.status` | The optional HTTP status, including a gateway's answer or a `429`. `nil` means no status was kept: headers may have arrived before reading the body failed. | +| `LastFailure.text` | The start of a text body, with whitespace collapsed, cut to 200 Unicode scalar values and followed by `...` if cut. `nil` for an empty body or one that isn't text. | + +An answer to the write or a refusal clears the streak and failure details. Setting it aside as `.failingAlone` keeps the failure details for your app to show. Releasing the write clears both at once, before the next answer. + ## Keep a view current A `LiveQuery` reads from the working copy and reads again whenever the copy changes, so a SwiftUI view that reads its `value` stays current with no refresh code. Create one on the main actor, in the model that owns the copy, and give it a `Reading`: diff --git a/content/docs/catalog/field-kinds.mdx b/content/docs/catalog/field-kinds.mdx index d62aad30..d8d8b019 100644 --- a/content/docs/catalog/field-kinds.mdx +++ b/content/docs/catalog/field-kinds.mdx @@ -6,10 +6,10 @@ description: The kinds of value a type's field can hold. | Kind | Holds | | ----------- | ------------------------------------------------------------------------------------------------------------------------------ | | `string` | Text, up to the field's `maxLength` in UTF-16 code units, or 100,000 where it sets none. | -| `number` | A number. | -| `integer` | A whole number. | +| `number` | A number. A whole number must be one Marfa serves back with the same digits, as the notes below say. | +| `integer` | A whole number, with the same rule on its digits. | | `boolean` | `true` or `false`. | -| `url` | A URL. | +| `url` | An absolute URL that the WHATWG URL Standard accepts, as the notes below say. | | `email` | An email address. | | `datetime` | A date as `YYYY-MM-DD`, or a date and time with a zone, such as `2026-03-01T09:30:00Z`. | | `date` | A calendar date as `YYYY-MM-DD`. | @@ -19,3 +19,7 @@ description: The kinds of value a type's field can hold. | `thumbnail` | A small image as a PNG, JPEG or WebP data URI that decodes to at most 16 KiB. | A `string` field can also name a `format` that only labels it: `bcp47` for a language tag, as on `language`, and `iso3166` for a country code, as on `country`. + +**Marfa refuses a whole number it can't serve back with the same digits.** It holds a number as a 64-bit float and serves it in the shortest digits that name that float, as JavaScript's `Number.prototype.toString` writes them. A write that names `1500000000000000001`, `1152921504606846976` (2^60) or `-0` is refused `400 invalid_properties`, because Marfa would serve them as `1500000000000000000`, `1152921504606847000` and `0`. `details.errors` names the value in `field`, such as `list.1`, with the message `Must be a whole number that Marfa serves back with the same digits, not , which it serves as . Send it as a string.` A bulk write is refused whole, and `details.index` gives the entry's position. A number Marfa serves with the digits sent, such as `1152921504606847000`, and a number with a fraction or an exponent are taken. **The rule covers every property value at any depth**, an edge's `properties` and an extension's data too, and a device refuses the same numbers before it queues the write. A value that must keep every digit goes in a `string` field. + +**Marfa judges a `url` value as the WHATWG URL Standard does.** It first trims both ends with JavaScript's `String.prototype.trim`, which removes U+FEFF but not U+0085. It then accepts the value where the standard parses it as an absolute URL, whichever Node.js version runs the server. For a URL with a special scheme, such as `http` or `https`, a host that is ASCII after percent-decoding is taken whatever its labels hold, so `http://xn--` is a URL. A host with a character that isn't ASCII must be valid IDNA, so `http://bücher.xn--` is refused. A host with a forbidden domain code point, or a last label that is a number where the host isn't a valid IPv4 address, is refused too. Marfa answers each refusal `400 invalid_properties`. Marfa stores the value without the whitespace it removed and without any tab, line feed or carriage return. A device judges the same way, with one exception: it refuses a special-scheme URL whose host holds a code point its own IDNA tables don't assign, whether or not the server takes the value. diff --git a/content/docs/guides/authenticate.mdx b/content/docs/guides/authenticate.mdx index daf4300c..5d923925 100644 --- a/content/docs/guides/authenticate.mdx +++ b/content/docs/guides/authenticate.mdx @@ -9,16 +9,37 @@ The owner signs in in a browser to manage the instance and approve apps. For dat **The owner signs in in a browser at `/auth/sign-in`**, using the password chosen when claiming the instance or a registered passkey. -To add a passkey, open `/auth/owner/passkeys` with a browser session authenticated within the last five minutes. This page also lists and removes individual passkeys. **Renaming or removing a passkey requires authentication within the last five minutes**; listing them needs only a live owner session. Removing one prevents further sign-in with it, while existing sign-ins and other passkeys keep working. +**The Password and passkeys page, at `/auth/owner/passkeys`, changes the password and manages passkeys.** It asks for the current password to change it. It adds a passkey, and lists, renames and removes the ones you have, showing when each was added in your browser's time zone. **Adding, renaming or removing a passkey needs a sign-in within the last five minutes**; listing them needs only a live owner session. If your sign-in is older, the page asks you to confirm it's you, as [the next section](#confirm-its-you) describes. Removing a passkey prevents further sign-in with it, while existing sign-ins and other passkeys keep working. + +**Home, at `/`, is where you land after you sign in**, and [Start from Home](#start-from-home) describes it. On the instance's own machine, `marfa --socket owner passkeys list` lists the passkeys. Use `marfa --socket owner passkeys rename --name "Laptop"` to rename one, or `marfa --socket owner passkeys remove ` to remove it. `marfa --socket owner passkeys add` opens the ordinary sign-in page, where you sign in with the password or a passkey, and then the passkey page; `--no-browser` prints the URL instead. **The add command issues no sign-in credential**: you still authenticate in the browser and register the passkey there. [Recover the owner's password](/guides/manage-keys#recover-the-owners-password) remains the way to recover a lost password. Passkeys need a supported browser and HTTPS, or localhost for development. **A valid passkey works even when password attempts are locked.** It creates the same owner browser authority used to approve apps and manage the instance; an app still needs its own token to read and write data. -The owner's browser and the private local command can list, rename and end sign-ins at `/owner/sign-ins`. **Keys and app tokens cannot use these operations.** Browser changes require authentication within the last five minutes. +The owner's browser and the private local command can list, rename and end sign-ins at `/owner/sign-ins`. **Keys and app tokens cannot use these operations.** A browser can end its own sign-in at any time. Ending another sign-in needs a sign-in within the last five minutes. `revoke_keys` applies only to an app: naming a browser or a key with `revoke_keys` set to `true` answers `400 validation_error`. + +## Confirm it's you + +**When a change needs a sign-in from the last five minutes and yours is older, the page asks you to confirm it's you.** A dialog titled "Confirm it's you" asks for the owner's password or a passkey. When you confirm, the page sends the change again, and you don't sign in a second time. + +Confirming replaces your browser session with a new one created at that moment, and ends the old one. The password counts against the same attempt windows as a password sign-in. A wrong password answers `401 unauthorized`, and once the windows are spent the answer is `429 rate_limited` with `Retry-After`. A passkey still confirms. A refused confirmation leaves your session as it was. These changes ask for it: + +- Adding, renaming or removing a passkey. +- Changing the owner's name or email on the Profile page. +- Revoking a key, disconnecting an app, or signing out another browser. +- Approving a write or a permission the app doesn't already hold, as [When approval needs a recent sign-in](#when-approval-needs-a-recent-sign-in) describes. + +A program that makes the same request, with no page to show a dialog, gets `403 forbidden` with the message `Sign in again to perform this operation`, and Marfa changes nothing. `POST /auth/confirm` is the operation behind the dialog: it takes the password or a passkey assertion of the owner who is signed in. A program that carries the owner's live browser session can call it with that proof, and a request with no live owner session, or with a key, is refused `401 unauthorized`. [Manage keys](/guides/manage-keys) covers key access, and `marfa sign-ins --help` lists the local sign-in commands. +## 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. + +**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. + ## Change the owner's email or name **The owner controls the sign-in address.** On the instance's machine, use its private socket to change the email, display name, or both: @@ -142,7 +163,7 @@ Then open the authorization endpoint in the owner's browser, with the challenge GET /auth/oauth2/authorize?response_type=code&client_id=&redirect_uri=https%3A%2F%2Freader.example.com%2Fcallback&scope=core.note%3Aread%20core.note%3Awrite%20offline_access&code_challenge=&code_challenge_method=S256&state= ``` -The owner signs in if they need to, and sees each scope the app asks for. They can untick any of them. A browser that signed in more than five minutes ago can approve reading only, as [the recent sign-in rules](#when-approval-needs-a-recent-sign-in) say. When they approve, Marfa sends the browser to your redirect address with a `code` and your `state`: +The owner signs in if they need to, then chooses what to allow on the Approve access screen, as [What the owner chooses](#what-the-owner-chooses) describes. When they approve, Marfa sends the browser to your redirect address with a `code` and your `state`: ```text https://reader.example.com/callback?code=&state=&iss=http%3A%2F%2Flocalhost%3A8600%2Fauth @@ -250,7 +271,7 @@ client_id=&scope=core.note%3Aread%20offline_access } ``` -Show the owner the `user_code` and `verification_uri`, or open `verification_uri_complete` for them. They sign in there and approve each requested scope, [with a recent sign-in](#when-approval-needs-a-recent-sign-in) for anything beyond reading. +Show the owner the `user_code` and `verification_uri`, or open `verification_uri_complete` for them. They sign in there, enter the code if it wasn't in the address, and choose what to allow on the same controls as the Approve access screen, under the [same rule for a recent sign-in](#when-approval-needs-a-recent-sign-in). The code page takes the code in any case, with or without a hyphen after the fourth character, as `LFMC-H9Q4` or `LFMCH9Q4`, and says that codes end after 10 minutes. **Keep the request explicit:** omitting `scope`, or sending only whitespace, is refused with `400 invalid_scope` before a code is issued. Read the approved `scope` in the token response; it can be narrower than you asked for. @@ -273,19 +294,29 @@ Until the owner decides, Marfa answers `400 authorization_pending`, and `400 slo -## When approval needs a recent sign-in +## What the owner chooses + +**The Approve access screen, and the device approval page, offer a choice for what the app asks and nothing more.** The screen is headed by the app's name and the instance's. A web page's origin is shown without its `https://`. + +- **Your data** has one control for each item type the app asks for, with the levels No access, Read only and Read and edit. A level is offered only up to the highest the app asked for, so an app that asks to read a type is never offered editing. Read and edit includes adding and deleting items. +- **Your profile** appears when the app asks for the profile, and **More control over Marfa** when it asks for a [permission](/catalog/permissions). Each permission starts turned off, and the three that can't be undone, `instance.maintain`, `schema.write` and `items.purge`, are marked. +- **Already allowed** lists what the app held before it asked again. Approving never lowers it, and the screen offers no level below it. -**A browser that signed in more than five minutes ago can approve reading only.** That covers the consent screen and the device approval screen alike. The scopes that read are `:read` for an item type or a pattern of types, `edge.:read` for an edge type or a pattern of edge types, `metadata:read`, `metadata.:read`, `content:read`, `profile:read`, `profile.:read`, `openid`, `profile`, `email` and `offline_access`. +A control for an item type shows the type's identifier under its label when two types share a label, such as `core.task` and `aic.ticket.task`, which are both "Task". -To approve write access, or any of the [permissions](/catalog/permissions) the app doesn't already hold, the owner must have signed in within the last five minutes. **A scope the owner approved for the app before needs no new sign-in.** +**An app that asks for all data with `*:read` or `*:write` can be given types instead.** The screen then offers Choose types beside the levels, with one control for each registered item type that the app's registration can request by name. It leaves out types under `system` and `marfa`, and types the app already holds in full. The token holds the types the owner turned on, such as `core.note:read core.task:read`, and not `*:read`, so read the approved `scope` in the token response. Turning on no type is declining. + +Marfa grants what the owner turned on, the reads that a write includes, and the scopes the request names that the app still holds when the owner approves. A form can only narrow the request: one that posts a scope the request didn't name is refused `400` and grants nothing, with the types chosen in place of all data as the exception, such as `core.note:read` under `*:read`. **Approving is declining when the request asks for access beyond `openid` and `offline_access` that the app doesn't already hold, and the owner turns all of that off.** The app then gets `access_denied` and keeps what it held. A request for nothing more than those two offers nothing to turn off, and the owner can approve it. + +The exact rules are in the contract: [`keys-and-oauth/approval-beyond-shown`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#keys-and-oauthapproval-beyond-shown) and [`keys-and-oauth/approval-types-beyond`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#keys-and-oauthapproval-types-beyond) for what a form may post, [`keys-and-oauth/approval-held-now`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#keys-and-oauthapproval-held-now) for the held scopes, and [`keys-and-oauth/consent-nothing-denies`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#keys-and-oauthconsent-nothing-denies) for when approving is declining. + +## When approval needs a recent sign-in -Marfa asks the owner for the password or a passkey, then returns them to the same screen: +**Approving can need a sign-in from the last five minutes.** Whether it does depends on the access the owner turns on that the app doesn't already hold, not on what the app asked for: turning on only reading never needs one, and a write or a permission the app doesn't already hold does. The Approve access screen and the device approval page follow the same rule. -- **The screen is opened after the five minutes.** Marfa sends the owner to sign in first, then back to the screen. -- **The screen was already open when the five minutes passed.** Approving sends the browser to sign in again and back to the same request, if the request hasn't expired. An authorization request is signed for a short window, and a device code lasts `expires_in` seconds. -- **The caller doesn't ask for a page.** A request whose `Accept` header doesn't prefer `text/html` is refused `403 forbidden`. +When the owner's sign-in is older, the page asks them to [confirm it's them](#confirm-its-you) as they press Approve, and approves in the same step. An approval sent without a recent sign-in, as a program might send it, is refused `403 forbidden` with the message `Sign in again to perform this operation`, and Marfa issues no code. -The app does nothing differently. An app that uses the authorization code grant waits for the redirect, and an app that uses the device grant keeps polling and gets `authorization_pending` until the owner decides. [Keys and sign-in](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#keys-and-oauthconsent-older-reads) holds the exact rules, starting with `keys-and-oauth/consent-older-reads`. +The app does nothing differently. An app that uses the authorization code grant waits for the redirect, and an app that uses the device grant keeps polling and gets `authorization_pending` until the owner decides. [Keys and sign-in](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#keys-and-oauthconsent-recent-needed) holds the exact rules in `keys-and-oauth/consent-recent-needed` and `keys-and-oauth/consent-older-reads`. ## Revoke access @@ -300,7 +331,7 @@ 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}`. -**Disconnecting an app from a browser needs a sign-in within the last five minutes.** A browser that signed in earlier 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. +**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. The endpoints under `/auth/oauth2/` and `/auth/device/` **answer errors in OAuth's own shape**, such as `{ "error": "invalid_grant", "error_description": "invalid refresh token" }`, not in the [error envelope](/api-reference/errors) the rest of the API uses. diff --git a/content/docs/guides/connect-a-web-page.mdx b/content/docs/guides/connect-a-web-page.mdx index a3102e12..579336cf 100644 --- a/content/docs/guides/connect-a-web-page.mdx +++ b/content/docs/guides/connect-a-web-page.mdx @@ -108,7 +108,7 @@ authorize.search = new URLSearchParams({ open(authorize, "marfa-approval", "popup,width=480,height=640"); ``` -The window asks the owner to sign in if they need to, and to sign in again if the page asks for more than reading and the owner signed in more than five minutes ago. It then shows the page's name, a caution that Marfa has not verified it, **the page's origin**, and each scope it asks for. The owner can untick any scope. Approving allows that origin to call the instance from a browser, as the next section explains. +The window asks the owner to sign in if they need to. It then shows the page's name, a caution that Marfa has not verified it, **the page's origin**, and a choice for each thing it asks for, as [What the owner chooses](/guides/authenticate#what-the-owner-chooses) describes. If the owner turns on access beyond reading that the page doesn't already hold, and signed in more than five minutes ago, the owner confirms it's them when they approve. The owner can turn off or lower any access the page newly asks for. Access the page already holds stays as it is, and the window offers no level below it. Approving allows that origin to call the instance from a browser, as the next section explains. @@ -172,7 +172,7 @@ const token = await ( } ``` -**A code works once**, and `scope` lists what the owner left ticked, which can be less than the page asked for. Marfa answers this request with `Access-Control-Allow-Origin` naming the page's origin only after the owner approves it, so a page that skips the approval can't read the answer. +**A code works once**, and `scope` lists what the owner chose, which can be less than the page asked for. Marfa answers this request with `Access-Control-Allow-Origin` naming the page's origin only after the owner approves it, so a page that skips the approval can't read the answer. diff --git a/content/docs/guides/deploy.mdx b/content/docs/guides/deploy.mdx index 34ab2f72..5696e64f 100644 --- a/content/docs/guides/deploy.mdx +++ b/content/docs/guides/deploy.mdx @@ -152,7 +152,7 @@ The CLI prints the link as `url`, at the address `MARFA_AUTH_BASE_URL` names. ** ## Run it without a container -In a clone of the repository, build the server: +**The server needs Node.js 22.14 or later.** The container image runs Node.js 24. In a clone of the repository, build the server: ```bash pnpm install --frozen-lockfile @@ -185,4 +185,6 @@ A server that crashes can leave its socket file behind. On restart, the server r `GET /` answers the `release` the image was built as, beside `version`, which is the commit. A build from a clone has no `release`. +In a browser, the same address answers a page instead of the description: [Home](/guides/authenticate#start-from-home) for the signed-in owner. A program that doesn't ask for HTML gets the description. + The rules behind this page are in the [instance chapter](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance.md) of the contract, and the container recipe is described in the [deployment guide](https://github.com/withmarfa/marfa/blob/main/deploy/README.md). diff --git a/content/docs/guides/keep-folders-running.mdx b/content/docs/guides/keep-folders-running.mdx index 083abbcd..837aad63 100644 --- a/content/docs/guides/keep-folders-running.mdx +++ b/content/docs/guides/keep-folders-running.mdx @@ -26,7 +26,7 @@ Every line names the folder's directory, and with `--json` each line is one JSON **The watch follows the list.** A folder you add with `marfa folders add` joins it, and one you remove leaves it, with no restart. Each folder uses the server its copy came from and the credential the keychain keeps for that server, so folders on different servers share one watch and no key is sent to a server it wasn't made for. -**One folder that can't be watched doesn't stop the others.** The watch reports it once and tries again. A folder fails to open while its first sync waits for `marfa folders confirm`, while another process works it, or when the keychain holds no credential for its server. Folders whose directories can't be found, such as one on a disk that isn't connected, are reported together in one line. They stay listed, and `marfa folders remove` takes one off. +**One folder that can't be watched doesn't stop the others.** The watch reports it once and tries again. A folder fails to open while its first sync waits for `marfa folders confirm`, while another process works it, or when the keychain holds no credential for its server. Folders whose directories can't be found, such as one on a disk that isn't connected, are reported together in one line. They stay listed, and `marfa folders remove` takes one off. A folder whose path now holds a different directory stays listed too, and the watch carries on with it when the original returns, as [Sync a folder](/guides/sync-a-folder#a-missing-or-unreadable-directory) describes. To watch a single folder, name its directory: `marfa folders watch Notes`. diff --git a/content/docs/guides/manage-keys.mdx b/content/docs/guides/manage-keys.mdx index 210b66b4..b69c0101 100644 --- a/content/docs/guides/manage-keys.mdx +++ b/content/docs/guides/manage-keys.mdx @@ -5,7 +5,19 @@ description: Give each app or script a key that reaches only what it needs, make A script or service you run yourself uses a key, which reaches only what it was made with. An app can instead sign in with OAuth, as [Authenticate](/guides/authenticate) shows. A notes app needs notes and nothing else; a script that runs for a week needs a key that stops working after a week. [Access](/access) explains the maps and permissions a key holds. -Two kinds of caller can make keys: the owner, through the instance's private socket or signed in to the instance's Manage Marfa pages in a browser, and a key or an app's token that holds the `keys.mint` permission. The CLI examples use the socket, so run them on the instance's machine, under the account that runs the server. +Two kinds of caller can make keys: the owner, through the instance's private socket or signed in to the instance's API keys page in a browser, and a key or an app's token that holds the `keys.mint` permission. The CLI examples use the socket, so run them on the instance's machine, under the account that runs the server. + +## Use the API keys page + +**The owner can make, narrow and revoke keys in a browser.** Open API keys from [Home](/guides/authenticate#start-from-home), at `/auth/owner/keys`. The page lists the keys, and Create key makes one: + +- **Details** holds the key's name and when it ends. +- **Access** starts with everything turned off. Turn on only what the key needs, at the levels No access, Read only or Read and edit, as the approval screen offers them for an [app](/guides/authenticate#what-the-owner-chooses). +- **Advanced** holds the source the key writes as, the other sources it may write as, where its items land, whether it follows Marfa's write rules, and its access to what connectors attach to your items. + +**The page shows the key once.** After you create it, Copy your key holds the secret, and the page takes it away when you leave. No other page under `/auth/owner/keys` ever holds a key's secret. Opening a key shows its details and what it holds, and you can give it less access or an earlier end, never more: to give more, make a new key. The Save bar appears after a change and counts the changes, and Revoke this key sits apart, below the form. The page refuses to save over a key that changed after the page opened, and asks you to reload. + +The rest of this page uses the socket and the API, which reach the same operations. ## Make a key for an app or a script @@ -28,11 +40,12 @@ Content-Type: application/json } ``` -Marfa answers the key's plaintext, which begins `marfa_k1_`, once. No later answer shows it, so **copy it to the app now**. +Marfa answers the key's plaintext, which begins `marfa_k1_`, once, and tells caches not to keep the answer (`Cache-Control: no-store`). No later answer shows it, so **copy it to the app now**. ```text marfa_k1_614e182634b2e78eb69e0a3cda8806b739be5a4917c5765a5420dfe766c63947 key 01a12311-8b01-7c3c-879e-3ff7346ce72f (Notes app); the plaintext is shown once, keep it now + core.note Read and edit ``` The source, here `notes-app`, is stamped on every item the key creates, so you can always tell which app wrote what. **Each key in use has a source of its own**, and a source another key already has is refused `409 conflict`. With this key, creating a note works, and creating a bookmark is refused `403 type_not_permitted`. @@ -164,6 +177,8 @@ Content-Type: application/json A key holding `keys.mint` changes only the keys that hold nothing it doesn't. A key holding `keys.manage` reaches every key, but can only take access away. +**An update can say it must only narrow.** Add `"narrow_only": true` to `PATCH /keys/{id}`, and Marfa refuses `403 forbidden` any change that would give the key more, whoever sends it: a permission, a map entry or a claimed source it doesn't hold, a later `expires_at` or none for a key that has one, or an `enforcement_override` other than its own. The limits on access and on `expires_at` also apply, without `narrow_only`, to a key that an app made and to a key changed by a caller holding `keys.manage`. The limit on `enforcement_override` applies only with `narrow_only`. The API keys page sends `narrow_only` with every save, so a page opened before another change can't undo it. + ## Revoke a key **Revoking ends a key at once**: the next request it sends is refused `401 unauthorized`. @@ -177,7 +192,7 @@ DELETE /keys/01a12311-8b01-7c3c-879e-3ff7346ce72f Authorization: Bearer ``` -**Revoking a key from a browser needs a sign-in within the last five minutes.** A browser that signed in earlier is refused `403 forbidden` and leaves the key working. The private socket and a key that holds `keys.mint` or `keys.manage` are not affected by when the owner last signed in. +**Revoking a key from a browser needs a sign-in within the last five minutes.** The API keys page asks a browser that signed in earlier to [confirm it's the owner](/guides/authenticate#confirm-its-you). A request without a page is refused `403 forbidden` and leaves the key working. The private socket and a key that holds `keys.mint` or `keys.manage` are not affected by when the owner last signed in. Revoke a key you think has leaked, then make a new one for the app. diff --git a/content/docs/guides/receive-webhooks.mdx b/content/docs/guides/receive-webhooks.mdx index 7452da68..5bb2d662 100644 --- a/content/docs/guides/receive-webhooks.mdx +++ b/content/docs/guides/receive-webhooks.mdx @@ -200,6 +200,13 @@ await marfa.POST("/webhooks/{id}/deliveries/{delivery_id}/redeliver", { **Only a `dead_letter` delivery can be sent again**; any other answers `409 conflict`. A delivery's record is kept for as long as the instance keeps its audit log. +## Change what a webhook sends + +[Update a webhook](/api-reference/webhooks/updateWebhook) changes its `url`, its `events`, its `type_filter` or whether it is `active`. **A change ends the pending deliveries it no longer allows**, so a receiver is never sent an event the owner has since excluded. + +- **Narrowing `events` or `type_filter`** settles each pending delivery they no longer select as `canceled`, with the error `The subscription's events or type filter no longer select this event.`, before the update answers. A delivery being sent when the change commits ends the same way. Widening them again revives nothing already settled, and a change that still selects an event settles nothing. +- **Changing the `url` or setting `active` to `false`** settles every pending delivery as `canceled`, so none goes to the old address. + ## Receive deliveries on a private network By default, **Marfa posts only to public addresses**. A host that isn't public is a loopback address such as `localhost`, a private address, a link-local or unique-local address, or a carrier-grade NAT address such as the `100.64.0.0/10` range that Tailscale uses. Marfa refuses a webhook whose `url` has such a host, both when you register it and when you change its `url`, with `400 validation_error`. The message names the setting that allows it, `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES`. diff --git a/content/docs/guides/swift-app.mdx b/content/docs/guides/swift-app.mdx index 35dc0244..64451dcf 100644 --- a/content/docs/guides/swift-app.mdx +++ b/content/docs/guides/swift-app.mdx @@ -220,6 +220,8 @@ Add the functions that write, and the one that sends the queue: `items.create` and `items.update` **return at once**: the change shows in the working copy, and the write waits in the queue. An `Edit` carries the version of the note the app read, so the instance can tell whether someone else changed it since, and it changes only the title. +If you add numeric properties, **a whole number Marfa would serve back with other digits throws `MarfaError.validation` with code `invalid_properties`**, before it queues the write. Send a value that must keep every digit as a string; `handle(_:)` shows the validation message. + `send()` drains the queue: it sends the writes in order and reports what the instance answered. If the server can't be reached, the report says so in `unavailable`, and **the writes stay queued for later**. Each answered write has a verdict, and the app tells you about the three that change what you see: `merged`, `conflicted` and `refused`. Then `forgetAnswered()` clears the answered writes, so `unsent` holds only what's still to send or needs attention. `apply(_:)` reads a report, so the next step can reuse it for the drains the follow runs. @@ -406,6 +408,12 @@ struct NoteRow: View { Press Command-B to build. The project builds for both My Mac and the iOS simulators. +**A write blocked as `BlockedReason.failingAlone` needs attention**: it keeps failing while the server answers other writes. `describe(_:)` shows its reason. Release the write to try again, or withdraw it. + +To show what happened, read `QueuedWrite.lastFailure`, an optional `LastFailure` with `status` and `text`; `environmentalStreak` counts the qualifying drains. The [Swift SDK reference](/swift-sdk#keep-the-copy-in-step) gives their meaning and when they clear. + +If you replace the reason's text with a switch over `BlockedReason`, handle `.failingAlone`: **an added enum case breaks an exhaustive switch** without a default. + diff --git a/content/docs/guides/sync-a-folder.mdx b/content/docs/guides/sync-a-folder.mdx index 5baf5ad5..4055ea09 100644 --- a/content/docs/guides/sync-a-folder.mdx +++ b/content/docs/guides/sync-a-folder.mdx @@ -210,6 +210,8 @@ A walking tour of the old harbor. The file took its type from the folder's settings, its title from its name, and its `marfa_id` once the instance created the item. A `.txt` file is all body. Any other file the search admits, such as a photo, becomes a file item, with its bytes uploaded as a blob, as [Handle files](/guides/handle-files) describes. +**A file bound to a file item keeps its bytes, whatever its name.** When a file is bound to a file item and sits at a name ending in `.md`, `.markdown` or `.txt`, Marfa sends its bytes however they begin: it reads no frontmatter in it and never flags it `unreadable` or `encoding`. If the file is empty, it waits until it holds bytes, and the other files in the scan still go. A file at such a name that isn't bound to a file item, a new file included, is read as a document, as above. + ## Edits on either side Edit a file and sync, and the edit reaches its item: @@ -257,7 +259,9 @@ The first line of a report counts files. A file whose create or edit was sent ea A scan that cannot read or identify an entry is incomplete. **It does not treat files it failed to see as deleted**, and it defers deletions already waiting until a complete scan succeeds. Fix the directory or disk access and sync again. -Each directory has its own working copy and queue, recorded in the machine's folder registry. **Adding a folder inside a registered folder, or around one, is refused.** +Each directory has its own working copy and queue, recorded in the machine's folder registry, together with an identity of the directory it admitted. **Adding a folder inside a registered folder, or around one, is refused.** + +**A folder stays listed when another directory takes its place.** If a restore or a sync tool replaces the directory with another, empty at first, Marfa keeps the folder's entry and doesn't read the new directory as the folder. When the original directory returns to its path, a watch of every folder carries on with it, and you don't add the folder again. Marfa removes an entry when the directory at its path holds no folder state and isn't known to differ from the one admitted. `marfa folders list --json` gives each entry a `root`, an opaque value to compare for equality and not to parse. A scan also leaves another folder's state directory and contents alone. If the registry cannot be read, deletion waits when the device cannot rule out a file having moved. diff --git a/content/docs/guides/troubleshooting.mdx b/content/docs/guides/troubleshooting.mdx index 531e691f..48a48347 100644 --- a/content/docs/guides/troubleshooting.mdx +++ b/content/docs/guides/troubleshooting.mdx @@ -116,9 +116,9 @@ Give the key that grant, or use a key that holds it. [Manage keys](/guides/manag -The owner's browser signed in more than five minutes ago, and the operation needs a recent sign-in. Revoking a key, disconnecting an app, and approving write access or a permission for an app are such operations. Marfa changes nothing. +The owner's browser signed in more than five minutes ago, and the operation needs a recent sign-in. Revoking a key, disconnecting an app, changing the profile or a passkey, and approving a write or a permission that an app doesn't already hold are such operations. Marfa changes nothing. -Sign in again at `/auth/sign-in?prompt=login`, then repeat the operation. Without `prompt=login`, the page only says you are signed in. An approval screen sends you to sign in and back when you open it or submit it. [Authenticate](/guides/authenticate#when-approval-needs-a-recent-sign-in) lists what a browser that signed in earlier can still approve. +A page in the owner's console handles this itself: it asks you to confirm it's you with the password or a passkey, then sends the change again. You see the message only from a program that makes the request, or when the confirmation is refused. A program can call `POST /auth/confirm` only if it carries the owner's live browser session and the owner's password or a passkey assertion. A bearer key, or a request with no live owner session, is refused `401 unauthorized`, so otherwise sign in again in a browser, or use the private socket. [Authenticate](/guides/authenticate#confirm-its-you) describes the confirmation, and [When approval needs a recent sign-in](/guides/authenticate#when-approval-needs-a-recent-sign-in) says which approvals need one. @@ -201,6 +201,8 @@ Cut text between characters, not between code units. In JavaScript, `String.prot The item's properties don't fit its type: a required field is missing, or a value has the wrong shape. `details.errors` names each field and what is wrong with it. A `core.note`, for example, needs a `body`. +Two other values are refused the same way. A whole number that Marfa wouldn't serve back with the same digits, such as `1500000000000000001`, is refused with the message `Must be a whole number that Marfa serves back with the same digits…`: send it as a string. A `url` value that the WHATWG URL Standard refuses, such as a host with a forbidden character, is refused too. [Field kinds](/catalog/field-kinds) gives both rules. + Read the type's fields with `marfa types get `, or `GET /types/`, and send what it asks for. [Items and types](/items-and-types) explains what a type defines. diff --git a/content/docs/guides/write-safely.mdx b/content/docs/guides/write-safely.mdx index 6b84f03e..6b642fd6 100644 --- a/content/docs/guides/write-safely.mdx +++ b/content/docs/guides/write-safely.mdx @@ -201,6 +201,8 @@ const send = () => If Marfa already answered that request, it returns the first answer again, with the header `Idempotency-Replayed: true`, and writes nothing. A repeat of a request refused `409 version_conflict` gets the same `409`. A repeat of one that failed with a `5xx` runs again, and so does a repeat of one refused `404 blob_not_found` because a file item named bytes its writer hadn't uploaded: Marfa keeps no answer for that refusal, so after you upload the bytes, the same key works. +**Marfa keeps only the status of a first answer larger than 1,048,576 bytes.** A repeat of that request answers `422 idempotency_result_not_retained`, with the first answer's status in `details.original_status`, and writes nothing again. A status from `200` to `299` there means the first write landed: read what it changed instead of sending it again. + A key names one request. The same key on a different request, such as the resolved write after a conflict, answers `422 idempotency_key_reused` and writes nothing, so **send each new write under a new key**. A repeat that arrives while the first is still running answers `409 idempotency_key_in_flight`: wait, then send it again. The item operations that write one item, the edge and folder operations, and [Apply a bulk action](/api-reference/bulk-actions/applyBulkAction) take a key. [Errors](/api-reference/errors#retrying) says which refusals to retry. diff --git a/openapi.json b/openapi.json index c4657862..15d78ac9 100644 --- a/openapi.json +++ b/openapi.json @@ -2264,7 +2264,7 @@ ], "description": "What happened to one entry of a bulk write." }, - "BulkAtomicRollbackOrMissingRequiredFieldOrValidationErrorRefusal": { + "BulkAtomicRollbackOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal": { "type": "object", "properties": { "error": { @@ -2274,6 +2274,7 @@ "type": "string", "enum": [ "bulk_atomic_rollback", + "invalid_properties", "missing_required_field", "validation_error" ], @@ -2444,7 +2445,7 @@ ], "description": "A bulk action Marfa runs in the background, with its progress." }, - "BulkCapExceededOrBulkConfirmationRequiredOrMissingRequiredFieldOrValidationErrorRefusal": { + "BulkCapExceededOrBulkConfirmationRequiredOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal": { "type": "object", "properties": { "error": { @@ -2455,6 +2456,7 @@ "enum": [ "bulk_cap_exceeded", "bulk_confirmation_required", + "invalid_properties", "missing_required_field", "validation_error" ], @@ -2684,6 +2686,44 @@ ], "description": "An item's extension namespaces that you can read." }, + "InvalidIdOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "invalid_id", + "invalid_properties", + "missing_required_field", + "validation_error" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } + }, + "required": [ + "code", + "message" + ], + "description": "What went wrong." + } + }, + "required": [ + "error" + ], + "description": "An error response." + }, "EdgeResponse": { "type": "object", "properties": { @@ -5620,6 +5660,44 @@ ], "description": "One page of a connector's agreements." }, + "InvalidPropertiesOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "invalid_properties", + "missing_required_field", + "unknown_type", + "validation_error" + ], + "description": "A machine-readable code for the error. Use it in your logic." + }, + "message": { + "type": "string", + "description": "A description of the error for a person to read. It can change, so don't match on it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about the error, such as the field it concerns. Each code defines its own details." + } + }, + "required": [ + "code", + "message" + ], + "description": "What went wrong." + } + }, + "required": [ + "error" + ], + "description": "An error response." + }, "FolderSearch": { "type": "object", "properties": { @@ -5685,7 +5763,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "Properties a new file takes where its frontmatter doesn't set them. A default for the type's body field is never taken: the file's body is that property." + "description": "Properties a new file takes where its frontmatter doesn't set them. A default for the type's body field is never taken: the file's body is that property. A whole number whose digits a 64-bit float doesn't keep is refused." }, "tags": { "type": "array", @@ -5730,7 +5808,7 @@ "additionalProperties": false, "description": "A removal waits to be confirmed when it takes more than `files` files and more than `fraction` of the folder's files." }, - "InvalidIdOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal": { + "InvalidIdOrInvalidPropertiesOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal": { "type": "object", "properties": { "error": { @@ -5740,6 +5818,7 @@ "type": "string", "enum": [ "invalid_id", + "invalid_properties", "invalid_transition", "missing_required_field", "unknown_type", @@ -8402,7 +8481,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "The item's properties, checked against the type's schema. If the instance's strict mode names the type, an undeclared property is refused as `invalid_properties` with `details.code` `unknown_property`." + "description": "The item's properties, checked against the type's schema. Strict mode refuses an undeclared property as `invalid_properties` (`details.code` `unknown_property`). A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." }, "id": { "type": "string", @@ -10220,7 +10299,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "The properties to write. They lay over the item's properties, or become all of them when `properties_mode` is `replace`. Strict mode refuses an undeclared property, as in `POST /items`." + "description": "The properties to write. They lay over the item's properties, or become all of them when `properties_mode` is `replace`. As in `POST /items`, strict mode refuses an undeclared property, and a whole number whose digits a 64-bit float doesn't keep is refused." }, "type": { "type": "string", @@ -14994,7 +15073,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "The item's properties, checked against the type's schema. If the instance's strict mode names the type, an undeclared property is refused as `invalid_properties` with `details.code` `unknown_property`." + "description": "The item's properties, checked against the type's schema. Strict mode refuses an undeclared property as `invalid_properties` (`details.code` `unknown_property`). A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." }, "properties_mode": { "type": "string", @@ -15110,11 +15189,11 @@ } }, "400": { - "description": "- `validation_error`: the body is malformed, has a key this operation doesn't declare, or has more than 5,000 entries.\n- `missing_required_field`: a required field is missing.\n- `bulk_atomic_rollback`: with `atomic` true, an entry was refused and nothing was written. `details.code` and `details.index` give its code and position. The status is the one that refusal carries alone.", + "description": "- `validation_error`: the body is malformed, has an undeclared key or has over 5,000 entries.\n- `missing_required_field`: a required field is missing.\n- `invalid_properties`: an entry's `properties` hold a whole number a 64-bit float can't keep.\n- `bulk_atomic_rollback`: an `atomic` entry was refused and nothing was written (`details.code`, `details.index`). The status is the refusal's own.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkAtomicRollbackOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/BulkAtomicRollbackOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -15626,7 +15705,8 @@ }, "patch": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "The properties to lay over each item's properties. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." } }, "required": [ @@ -15735,11 +15815,11 @@ } }, "400": { - "description": "- `validation_error`: the body or `filter` is malformed or has an undeclared key or an empty field, `update_tags` has neither `add` nor `remove`, or `expected_ids` is empty or not on a purge.\n- `missing_required_field`: a field the action needs is missing.\n- `bulk_confirmation_required`: a purge without `confirm: \"PURGE\"`.\n- `bulk_cap_exceeded`: more items match than `max_items` allows.", + "description": "- `validation_error`: the body is malformed or has an undeclared or empty field, or `update_tags` has no `add` or `remove`.\n- `missing_required_field`: a needed field is missing.\n- `invalid_properties`: `patch` holds a whole number a 64-bit float cannot keep.\n- `bulk_confirmation_required`: a purge without `confirm: \"PURGE\"`.\n- `bulk_cap_exceeded`: more items match than `max_items` allows.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkCapExceededOrBulkConfirmationRequiredOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/BulkCapExceededOrBulkConfirmationRequiredOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -18607,7 +18687,7 @@ "schema": { "type": "object", "additionalProperties": {}, - "description": "The namespace's new data, as a JSON object." + "description": "The namespace's new data, as a JSON object. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." } } } @@ -18644,11 +18724,11 @@ } }, "400": { - "description": "- `validation_error`: the body is not a JSON object, or its JSON is longer than 102,400 characters.\n- `invalid_id`: the ID is not a valid item ID.", + "description": "- `validation_error`: the body is not a JSON object, or its JSON is longer than 102,400 characters.\n- `invalid_id`: the ID is not a valid item ID.\n- `invalid_properties`: the body holds a whole number a 64-bit float can't keep.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -20587,7 +20667,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "The edge's properties. Leave it out for none." + "description": "The edge's properties. Leave it out for none. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." } }, "required": [ @@ -21449,7 +21529,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "Properties to merge over the edge's. Required unless an end moves." + "description": "Properties to merge over the edge's. Required unless an end moves. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." }, "source_id": { "type": "string", @@ -22355,7 +22435,7 @@ "properties": { "type": "object", "additionalProperties": {}, - "description": "The edge's properties. On an existing edge they merge over its own. Leave it out for none." + "description": "The edge's properties. On an existing edge they merge over its own. Leave it out for none. A whole number whose digits a 64-bit float doesn't keep is refused: send it as a string." }, "version": { "type": "integer", @@ -22426,11 +22506,11 @@ } }, "400": { - "description": "- `validation_error`: the body is malformed, has an undeclared key, or has more than 5,000 entries.\n- `missing_required_field`: `edges` is missing, or an entry lacks `source_id`, `target_id` or `edge_type`.\n- `bulk_atomic_rollback`: with `atomic` true, an entry was refused and nothing was written. `details.code` and `details.index` give its code and position. The status is that refusal's own.", + "description": "- `validation_error`: the body is malformed, has an undeclared key or has over 5,000 entries.\n- `missing_required_field`: a required field is missing.\n- `invalid_properties`: an entry's `properties` hold a whole number a 64-bit float can't keep.\n- `bulk_atomic_rollback`: an `atomic` entry was refused and nothing was written (`details.code`, `details.index`). The status is the refusal's own.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkAtomicRollbackOrMissingRequiredFieldOrValidationErrorRefusal" + "$ref": "#/components/schemas/BulkAtomicRollbackOrInvalidPropertiesOrMissingRequiredFieldOrValidationErrorRefusal" } } }, @@ -37265,11 +37345,11 @@ } }, "400": { - "description": "- `validation_error`: a setting is malformed, such as a `system.*` type, an invalid `filter` or a `first_placement` directory outside the folder. `details.errors[0].path` names it. A key the body or a setting doesn't declare is named in `details.unknown_body_fields`.\n- `missing_required_field`: `title` is missing.\n- `unknown_type`: a setting names a type that isn't registered.", + "description": "- `validation_error`: a setting is malformed or undeclared, such as a `system.*` type or a `first_placement` outside the folder. `details.errors[0].path` or `details.unknown_body_fields` names it.\n- `missing_required_field`: `title` is missing.\n- `unknown_type`: a setting's type is unknown.\n- `invalid_properties`: `defaults.properties` has a whole number a 64-bit float can't keep.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidPropertiesOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" } } }, @@ -37753,11 +37833,11 @@ } }, "400": { - "description": "- `validation_error`: a setting is malformed (`details.errors[0].path` names it), a key is undeclared (`details.unknown_body_fields` names it), the body names no setting, or the query has `conflict`.\n- `missing_required_field`: `version` is missing.\n- `unknown_type`: a setting names a type that isn't registered.\n- `invalid_id`: the ID is malformed.\n- `invalid_transition`: the folder is revoked.", + "description": "- `validation_error`: a setting is malformed or undeclared (`details.errors[0].path` or `details.unknown_body_fields` names it).\n- `missing_required_field`: `version` is missing.\n- `unknown_type`: a setting's type is unknown.\n- `invalid_properties`: `defaults.properties` has a whole number a 64-bit float can't keep.\n- `invalid_id`: the ID is invalid.\n- `invalid_transition`: the folder is revoked.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidIdOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" + "$ref": "#/components/schemas/InvalidIdOrInvalidPropertiesOrInvalidTransitionOrMissingRequiredFieldOrUnknownTypeOrValidationErrorRefusal" } } }, @@ -40751,11 +40831,15 @@ "string", "null" ], - "description": "Replaces when the key stops working, as an ISO 8601 date and time in the future. `null` clears it. Leave it out to keep it. It can't be later than your own key's `expires_at`, or the key's current one for a key an app made or through `keys.manage`." + "description": "Replaces when the key stops working, as an ISO 8601 date and time in the future. `null` clears it. Leave it out to keep it. It can't be later than your own key's `expires_at`, or the key's current one for a key an app made, through `keys.manage` or with `narrow_only`." }, "source": { "type": "string", "description": "Can't change. To give a key another source, create a new key and revoke this one." + }, + "narrow_only": { + "type": "boolean", + "description": "`true` to refuse the update if it would give the key more: a permission, map entry or claimed source it doesn't hold, a later `expires_at` or none, or an `enforcement_override` other than its own. Defaults to `false`." } }, "additionalProperties": false @@ -40852,7 +40936,7 @@ } }, "403": { - "description": "- `forbidden`: you don't hold `keys.mint` and aren't the owner or local command; the body gives the key a permission, map entry or source you don't hold, or an `expires_at` later than yours; it widens a key an app created or, through `keys.manage`, any key; or it names `enforcement_override` and you don't hold `config.manage`. `details.required_scope` or `details.source` names what's missing.", + "description": "- `forbidden`: you don't hold `keys.mint` and aren't the owner or local command; the body gives the key more than you hold or an `expires_at` later than yours; it widens or lengthens an app's key, or any key through `keys.manage` or with `narrow_only`, which also refuses a changed `enforcement_override`; or it names `enforcement_override` without `config.manage`. `details` names what's missing.", "content": { "application/json": { "schema": { @@ -44370,7 +44454,7 @@ "ownerSession": [] } ], - "description": "Ends a sign-in at once: it can't make another request, and an app can't refresh its tokens. The keys an app minted stay unless `revoke_keys` is `true`. Requires the owner's browser session, signed in within five minutes, or the local command.", + "description": "Ends a sign-in at once: it can't refresh or make another request. An app's keys stay unless `revoke_keys` is `true`. Needs the owner's browser session or the local command. A browser over five minutes old can end only itself, without `revoke_keys`.", "parameters": [ { "schema": { @@ -44496,7 +44580,7 @@ } }, "403": { - "description": "- `forbidden`: the request carries a key or an app's token rather than the owner's browser session or the local command's authority, or the browser signed in more than five minutes ago.", + "description": "- `forbidden`: the request carries a key or an app's token rather than the owner's browser session or the local command's authority, or the browser signed in more than five minutes ago and the ID names another sign-in or `revoke_keys` is `true`.", "content": { "application/json": { "schema": { @@ -48192,7 +48276,7 @@ "Webhooks" ], "summary": "Update a webhook", - "description": "Updates a webhook and returns it. Fields you leave out keep their values. Changing `url`, or setting `active` to `false`, cancels its pending deliveries. To change the secret, create a new webhook.", + "description": "Updates a webhook and returns it. Fields you leave out keep their values. Marfa cancels a pending delivery if you change `url`, set `active` to `false`, or change `events` or `type_filter` to exclude it. To change the secret, create a new webhook.", "security": [ { "bearerAuth": [] diff --git a/src/generated/GLOSSARY.md b/src/generated/GLOSSARY.md index 2af539f7..d990d271 100644 --- a/src/generated/GLOSSARY.md +++ b/src/generated/GLOSSARY.md @@ -70,6 +70,45 @@ The words are grouped by topic. | **local authority** | Authority supplied only by the private Unix socket to a process running under the server's operating-system account. It is not an API key and cannot be asserted through public HTTP. | | **setup code** | A machine-issued proof that authorizes one owner claim. It grants no API access and stops working when setup closes or its generation is replaced. | +### Access words + +Access is read the same way wherever a person sees it: on the pages where they approve it, on a key's page, on an app's page and in the CLI's output. For each kind of thing it is one level, and a short list of named powers. The server builds the description in `packages/server/src/routes/access-model.ts`, from the words in `scope-copy.ts` and `permission-labels.ts` beside it; the CLI keeps the same words in `core/marfa-cli/src/access.rs`; `ci/access-words.test.ts` holds both to the tables below. A row the table does not title, such as an item type or a link type, is named by its own label on a page and as the scope writes it in the CLI (`core.note`, `edge.references`). + +| Level | The word | +| ------- | ------------- | +| `none` | No access | +| `read` | Read only | +| `write` | Read and edit | + +| Row | The title | +| --------------------- | -------------------------- | +| `*` | All your data | +| `content` | Your content | +| `user.*` | Your custom types | +| `edge.*` | Links between items | +| `profile` | Profile | +| `profile.name` | Name | +| `profile.email` | Email address | +| `profile.avatar` | Avatar | +| `metadata` | Definitions on your server | +| `metadata.types` | Custom data types | +| `metadata.edge_types` | Custom relationship types | + +| Power | The title | +| ------------------- | ----------------------------------------------------------- | +| `keys.mint` | Make, see, edit and revoke keys | +| `keys.manage` | See, edit, narrow and revoke every key | +| `grants.manage` | See and disconnect other apps | +| `config.manage` | See and change Marfa's settings | +| `instance.read` | See health, metrics, storage and job status | +| `instance.maintain` | Run housekeeping, reset platform types and cancel bulk jobs | +| `schema.write` | Replace or remove type definitions | +| `blobs.manage` | Read and add any file, and manage file storage | +| `webhooks.manage` | Manage webhooks that send events to other services | +| `connectors.manage` | See, manage and remove all connectors | +| `audit.read` | Read the activity log | +| `items.purge` | Delete items in the trash for good | + ### The instance | Word | What it means | diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index 95f897a7..755e9375 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.17/marfa-0.0.17-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.18/marfa-0.0.18-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/cli/global-options.md b/src/generated/cli/global-options.md index 6f38331f..93178cb4 100644 --- a/src/generated/cli/global-options.md +++ b/src/generated/cli/global-options.md @@ -2,8 +2,9 @@ Every command accepts these. | Option | Description | | --- | --- | -| `--url ` | The server's base URL. Falls back to MARFA_API_URL, then to the server a kept credential made current. | -| `--key ` | A key or a token for that server. Prefer MARFA_API_KEY or the keychain: a command line is readable by other users of the machine and stays in shell history. Falls back to MARFA_API_KEY, then to the keychain: the file MARFA_KEYCHAIN names, where it names one. | +| `--url ` | The server's base URL. Falls back to MARFA_API_URL, then to the server a kept credential made current. A command that sends a key from --key or MARFA_API_KEY needs the URL named, and so does any command with the keychain off. | +| `--key ` | A key or a token for that server. Prefer MARFA_API_KEY or the keychain: a command line is readable by other users of the machine and stays in shell history. Falls back to MARFA_API_KEY, then to the keychain: the file MARFA_KEYCHAIN names, where it names one, unless the keychain is off. | +| `--no-keychain` | Treat the keychain as absent: read nothing from it and write nothing to it, so the server and the key must come from --url, --key, MARFA_API_URL or MARFA_API_KEY. A command that needs to keep something refuses. MARFA_NO_KEYCHAIN=1 does the same. | | `--allow-http` | Send a credential over plain http to a host that is not this machine without the warning, for a network that is private. MARFA_ALLOW_HTTP=1 does the same. | | `--socket ` | Use private local process authority through this Unix socket, without a keychain. | | `--json` | Print the answer as JSON, and a refusal as one JSON object on stderr, after any warning, which is one plain line. | diff --git a/src/generated/contract-index.json b/src/generated/contract-index.json index db330452..017ece03 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -10,15 +10,33 @@ "authentication/passkey-password-lock": "19e5fbf4a92d5049", "authentication/passkey-removal": "3cf0ca6cd05e51e7", "authentication/passkey-restart": "ff4dd2ca7c0a56f9", - "authentication/passkey-address": "072ac2a244ec282c", + "authentication/passkey-address": "dcf5e2b5f0e01050", "authentication/passkey-duplicate": "84e155ef6b4f85ad", "authentication/passkey-owner-only": "c626f5cf84ac99b4", "authentication/passkey-origin": "585591ff2d852436", "authentication/passkey-recent-management": "4872775ae6c4112f", "authentication/passkey-rename": "873a65d2824c5d11", + "authentication/passkey-registration-name": "e4529617d0e79edf", "authentication/passkey-management-not-found": "8af17181724212c9", "authentication/passkey-management-atomic": "b3b8acc6908ebac0", - "authentication/passkey-add-url": "5bb420ac86902958" + "authentication/passkey-add-url": "5bb420ac86902958", + "authentication/recent-refusal": "010cda700eeb99da", + "authentication/confirm-replaces": "e494d296e5b7a1b5", + "authentication/confirm-ends-old": "490b091e92e351b9", + "authentication/confirm-wrong-password": "13dab28ee1800518", + "authentication/confirm-lanes": "95714755d99ed182", + "authentication/confirm-locked": "2125f16bd33d220d", + "authentication/confirm-refused-keeps": "b9b3510d166cd4e5", + "authentication/confirm-names-no-account": "4f1286d50706cfd2", + "authentication/confirm-passkey-owner": "874f470a6620c4e4", + "authentication/confirm-needs-session": "ce8ac0c02906f879", + "authentication/confirm-origin": "248cf3e06973c5d4", + "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" }, "blobs": { "blobs/read-idle-lifetime": "343c013f840aa398", @@ -769,6 +787,12 @@ "device/save-before-sync-joins": "02b863a086eef3bc", "device/create-slice-tier": "e39b49cc543f808d", "device/declared-checked": "2566560af1b8f03b", + "device/number-exact": "1a5d91b7731c768a", + "device/number-exact-folder-flagged": "1bc25c70f8059113", + "device/number-exact-folder-kept": "f4abbcf9e7a313b9", + "device/number-digits": "e7d56d1cce146bd6", + "device/url-trimmed": "28313af0bd24a6d5", + "device/url-idna-unassigned": "2a9edf20a556e53d", "device/unknown-type-write": "69342d8d253b5913", "device/declaration-refused": "46d894c681563663", "device/declaration-refused-whole": "a4978e4678068722", @@ -795,7 +819,7 @@ "device/stop-no-retry": "6ef8eee75b47285d", "device/stop-first-request": "7a3ae9cb3b81afb3", "device/stop-first-request-unchanged": "258ff5288c2da4aa", - "device/tag-bound": "14523bfe935f38a3", + "device/tag-bound": "7b1e7174b8bc7493", "device/tag-count": "86ee8eeffb98f09e", "device/tag-bound-nothing-saved": "817aea68c4602642", "device/tag-bound-taken": "e1bf050475406c4c", @@ -1532,6 +1556,7 @@ "events/retry-poll-run": "5cb0290d41f8a694", "events/retry-restart-once": "3de84b2c2cf3c0e4", "events/cancel-edited": "41c7ab9a2feea7da", + "events/cancel-unselected": "888d9906e7f7f956", "events/cancel-deleted": "05aaa3087317b769", "events/cancel-record": "cc8dcbc457cac095", "events/redeliver-accepted": "9ddaf60bbdc7105c", @@ -1805,8 +1830,8 @@ "folders/time-null-empty": "076555860f525261", "folders/time-null-preserved": "5695c387a083ca0a", "folders/time-render": "efb491ce795d017c", - "folders/time-invalid": "9e2c8ec2d695bc10", - "folders/own-field-validation": "af443f0c9aefa7ae", + "folders/time-invalid": "42abab0244401a1e", + "folders/own-field-validation": "ddbcc6c42cb92c00", "folders/tag-state-forms": "2cc13528c991a383", "folders/preserve-source": "da94a142f4a06d6f", "folders/preserve-change-in-place": "c0c07d223491f9f6", @@ -1846,18 +1871,22 @@ "folders/document-name-no-writes": "47513baa0e261b66", "folders/document-name-continue": "0314bae960d92265", "folders/document-name-retry": "a11dd9df86245c7e", - "folders/unreadable-yaml": "b37046a97a2d4212", + "folders/unreadable-yaml": "2885c86952ae4e23", "folders/unreadable-no-write": "1e2ae833610fb8c9", "folders/unreadable-preserve": "ba1d3502556f580d", "folders/unreadable-report": "77b8a127d08e32eb", "folders/unreadable-rename": "af854e90cf8cd754", "folders/unreadable-repaired": "e3d5f5ed62eaf7f7", - "folders/encoding-flag": "ccae19d84c2335d8", + "folders/encoding-flag": "4c241ee47a3e9373", "folders/encoding-no-send": "c00d9872d5567b7d", "folders/encoding-preserve": "0da85bd5e7d9b381", "folders/encoding-status": "e7f95f6a7551df7d", "folders/encoding-repaired": "7a380fd7f643f199", "folders/file-bytes-not-document": "22dda5bee37721ff", + "folders/file-item-doc-name-sent": "face4bebd45344f3", + "folders/file-item-doc-name-unflagged": "f14624fbd75d79e8", + "folders/file-item-doc-name-emptied": "0bcc3bc1b41f6b6b", + "folders/file-item-doc-name-emptied-scan": "ba27c103d7f00b3a", "folders/version-line": "52e3d6e0d162afe3", "folders/version-stale-merge": "ebce032284cf0c02", "folders/version-absent-merge": "5b9d1faec2ac4dbd", @@ -1975,7 +2004,7 @@ "folders/embed-list-paragraph": "b6950ec034b0851c", "folders/embed-duplicate-line": "0f92efe120c81458", "folders/embed-upload-size": "8e9097cde63150a5", - "folders/markdown-extensions": "955cea5f28980bb9", + "folders/markdown-extensions": "7d13ca30c264866c", "folders/id-rename": "57d9fa591548614f", "folders/id-missing-binding": "0bf5a6f40bff5755", "folders/id-minted": "29592adc242c3768", @@ -1998,7 +2027,7 @@ "folders/id-unknown-new": "4eea10c487041eb6", "folders/id-system-ignored": "f5bf8cbdb94ab9f7", "folders/id-file-item-ignored": "e360cf24f4698a47", - "folders/raw-text-body": "3cf85c9a2b2f8d02", + "folders/raw-text-body": "d8b50f864566caa4", "folders/identity-rename": "1cd5ac0da2af6163", "folders/identity-unusable": "e2b366e0bdcc118d", "folders/identity-admitted-files": "b04df5b8f756fab2", @@ -2250,7 +2279,9 @@ "folders/remove-deleted-reported": "d614ae68ea571dc9", "folders/remove-unlisted-reported": "d8a26b9c4308e7ab", "folders/remove-unlisted-names-other": "683f7bd77e0aea68", - "folders/registry-removed-state": "828d221028083b8e", + "folders/registry-root-identity": "2582a0f7eb5ee76d", + "folders/registry-removed-state": "8fd1591f5af1b7c5", + "folders/registry-replaced-kept": "ee475b6966e8a153", "folders/registry-missing-kept": "0716971ee28a34e5", "folders/registry-unreadable-deletes-held": "662ca30f9c1ad7f1", "folders/registry-invalid-replaced-on-add": "54aac21a39c9cafa", @@ -2272,7 +2303,7 @@ "folders/uncertain-move-deferred": "26ddcaa6fb20dbd5", "folders/uncertain-move-deferred-unsure-reported": "a44b92875008474a", "folders/uncertain-move-not-text": "986c81a1f5d0d8b1", - "folders/moved-arrival-bytes": "291b20f2e114cf2b", + "folders/moved-arrival-bytes": "182a4887840a2089", "folders/moved-awaiting-peer": "e8ba48c290b6ec04", "folders/moved-awaiting-peer-no-replacement": "5bd0224352a549f5", "folders/watch-every-listed": "9b481488d7a7fba1", @@ -2283,6 +2314,7 @@ "folders/watch-every-others-go-on": "96a9f1359ddad31a", "folders/watch-every-missing-said": "e6121f2cb97d581e", "folders/watch-every-tried-again": "8bccfc61dce115ee", + "folders/watch-every-root-returns": "d97112af0e15293d", "folders/watch-every-credential-refused": "d1e8e316bbdccc14", "folders/watch-every-joins": "2c2c6aa4617d97ba", "folders/watch-every-leaves": "bc18ccdded59023b", @@ -2971,6 +3003,11 @@ "instance/root-page": "c1c32057467c83fa", "instance/root-description": "1a23cea416e66416", "instance/root-vary": "83d7f57713e419b5", + "instance/root-home": "50bdb68a4574ccb5", + "instance/root-home-search": "01113994fb342a31", + "instance/root-home-no-store": "07150c564aec3a48", + "instance/root-search-key": "f7d68a6991981226", + "instance/root-search-key-other": "c9c097224350f3ad", "instance/page-policy": "e1a892a64626051b", "instance/page-policy-nonce-only": "259228eb3bbefd69", "instance/page-policy-default-none": "f79682f7197d8b42", @@ -3087,6 +3124,9 @@ "items/property-length-cap": "57562925648f183e", "items/property-length-units": "64059eeb24118d15", "items/property-invalid": "df9a4fb4de90661f", + "items/property-number-exact": "bfde0be15944ce72", + "items/property-number-exact-bulk": "f6e73bdcef84f1eb", + "items/property-number-kept": "8063229297a2dddf", "items/property-name-empty": "53434991ae248cc1", "items/create-null-optional": "45736d3bdda52d4e", "items/property-order-create": "0ebfda02b3aaa2a4", @@ -3099,6 +3139,7 @@ "items/tags-empty": "c5979c0bc344a0c1", "items/tags-count": "f9f5b02c6931fceb", "items/tag-text": "8e92a2b85c1f0b78", + "items/tag-dot-segment": "0c7f8cbae600b094", "items/create-no-type": "d62ba775fc751523", "items/create-malformed-type": "75faf07479567aca", "items/create-unknown-type": "1cb37252ab035e16", @@ -3384,6 +3425,7 @@ "keys-and-oauth/mint-ceiling": "5c91f34ac349a810", "keys-and-oauth/namespace-ceiling": "9eaad2cc36929ad4", "keys-and-oauth/plaintext-once": "87e480b89a5cc72f", + "keys-and-oauth/plaintext-no-store": "3974e73945242ab0", "keys-and-oauth/own-source-unique": "cff6bb4f58cc4d61", "keys-and-oauth/own-source-race": "ba4f722ca2fc9eb7", "keys-and-oauth/mint-permission": "008802f8ba450886", @@ -3396,6 +3438,10 @@ "keys-and-oauth/update-ceiling-unchanged": "f0f8aa9e6c5c3ed7", "keys-and-oauth/manage-narrows-only": "87a75da5e4702508", "keys-and-oauth/app-key-narrows-only": "5a9cfb9cb9b0f725", + "keys-and-oauth/update-narrow-only": "98345011333f6d4a", + "keys-and-oauth/update-narrow-only-unchanged": "80051bbc9b9cfe42", + "keys-and-oauth/update-narrow-only-applies": "5ce0a93e67eb099b", + "keys-and-oauth/update-narrow-only-levers": "eb06f275bb0c85aa", "keys-and-oauth/key-unknown": "cc5aa0d3acd83314", "keys-and-oauth/key-malformed-id": "45676aecf585759c", "keys-and-oauth/revoke-answer": "8b230250f4155ac0", @@ -3438,6 +3484,7 @@ "keys-and-oauth/expiry-manage-narrows": "4dc55ec3b738bd73", "keys-and-oauth/expiry-app-key-narrows": "b15a4469a20d5f0a", "keys-and-oauth/expiry-owner-free": "a400c5602fef7d1a", + "keys-and-oauth/expiry-narrow-only-narrows": "b6270d65c69b2d45", "keys-and-oauth/no-credential": "d272f1da07e3f5e0", "keys-and-oauth/unknown-credential": "c55959c2200bd19c", "keys-and-oauth/permission-before-body": "f4a87220c641be6c", @@ -3508,14 +3555,17 @@ "keys-and-oauth/register-granted-kept": "45ba2033c9fb57d6", "keys-and-oauth/register-offered-kept": "398f18856c823f5c", "keys-and-oauth/no-backchannel-registered": "6a8a759e874f9714", - "keys-and-oauth/consent-screen": "061270f54be70f03", - "keys-and-oauth/consent-accepted": "f28c1e8771d0fdab", + "keys-and-oauth/consent-screen": "d27892df284d763b", + "keys-and-oauth/consent-accepted": "1f6c7122f504a25c", "keys-and-oauth/consent-declined": "f68b1619e4191282", - "keys-and-oauth/consent-ticked": "112dd15eb8440588", + "keys-and-oauth/consent-nothing-denies": "ac8a0ee0736f491a", + "keys-and-oauth/consent-ticked": "3acc2dab9abfa695", "keys-and-oauth/consent-standing": "44b74fa33945a1f6", - "keys-and-oauth/consent-wider": "830daf26ffb1cdc2", + "keys-and-oauth/consent-wider": "bd1e6b4845387025", "keys-and-oauth/consent-prompt": "62d7669cda1e33dd", "keys-and-oauth/authorize-narrowed": "99d695052c65f3fc", + "keys-and-oauth/authorize-narrowed-types": "b7e13fa6ac437614", + "keys-and-oauth/authorize-narrowed-refresh": "1d38d9b1c59a5156", "keys-and-oauth/authorize-nothing-grantable": "84f26fa217ed274b", "keys-and-oauth/authorize-pkce": "9fa7f38d8a9e000f", "keys-and-oauth/authorize-redirect-unregistered": "782f7a0d8824fbf0", @@ -3523,10 +3573,38 @@ "keys-and-oauth/prompt-none-consent": "2653bbfc6a75d432", "keys-and-oauth/prompt-none-code": "9b98c8a06b517db6", "keys-and-oauth/consent-older-reads": "3c6988333d5f9d12", - "keys-and-oauth/consent-recent-needed": "0c94fa1a0911d904", - "keys-and-oauth/consent-recent-browser": "904ebc41646ee60c", + "keys-and-oauth/consent-recent-needed": "52dffd78a15df3c0", "keys-and-oauth/consent-recent-held": "26fba6553ebae9e6", - "keys-and-oauth/consent-page-recent": "e982cc27a25f840b", + "keys-and-oauth/consent-page-confirm": "573d1168c0cf0267", + "keys-and-oauth/consent-page-reads": "30c161aa43200808", + "keys-and-oauth/approval-one-control": "acb6d5cb6c99480d", + "keys-and-oauth/approval-edit-words": "d108465dfda36ac0", + "keys-and-oauth/approval-levels-asked": "1e13de9608489631", + "keys-and-oauth/approval-only-asked": "29be86f83de99797", + "keys-and-oauth/approval-open-ended": "6abfcaff6aac61d3", + "keys-and-oauth/approval-powers-off": "31e2c858f3b34f42", + "keys-and-oauth/approval-powers-marked": "b1a72d2a5eded535", + "keys-and-oauth/approval-powers-words": "136193c47e4eaf08", + "keys-and-oauth/manage-powers-marked": "100241feb952e0b4", + "keys-and-oauth/approval-held-shown": "28cee7e209888407", + "keys-and-oauth/approval-levels-held": "d5e2e9e68f0a1728", + "keys-and-oauth/approval-keep-held": "8609376d97fac727", + "keys-and-oauth/approval-never-lowers": "a1eee9a92295696c", + "keys-and-oauth/approval-held-now": "f17b09f89210cbd7", + "keys-and-oauth/approval-write-read": "0fdde96611c94962", + "keys-and-oauth/approval-beyond-shown": "bab522ac4258d0b1", + "keys-and-oauth/approval-all-data-types": "d298c731ec8fa090", + "keys-and-oauth/approval-types-levels": "5f63759580a7341e", + "keys-and-oauth/approval-types-grant": "b674e4577f1790cb", + "keys-and-oauth/approval-types-unchosen": "b7795fd568306529", + "keys-and-oauth/approval-types-beyond": "8639d9fca9018a38", + "keys-and-oauth/approval-types-keep": "18addc0cd52e9e29", + "keys-and-oauth/approval-same-label": "47a1028348c70791", + "keys-and-oauth/approval-same-label-described": "a1c893ec53dfe266", + "keys-and-oauth/approval-own-label": "b02259c4ddebdf0b", + "keys-and-oauth/approval-page-row-no-identifier": "1301c06aa4026174", + "keys-and-oauth/access-words": "782a14594b362397", + "keys-and-oauth/access-words-cli": "30cb1eebe40e7f96", "keys-and-oauth/catch-up-signed-in": "29b3b37bcb0436ab", "keys-and-oauth/catch-up-nobody": "da931f67e026cc4c", "keys-and-oauth/catch-up-nobody-unchanged": "318f8b49b361567d", @@ -3589,16 +3667,17 @@ "keys-and-oauth/device-pending": "746c52329194ad61", "keys-and-oauth/device-slow-down": "bbea12d7ec67534e", "keys-and-oauth/device-denied": "4c4d5dc5f52decf6", + "keys-and-oauth/device-nothing-denies": "831e978dbb44226d", "keys-and-oauth/device-expired": "3dc71e8f6878d2a8", "keys-and-oauth/device-approved": "0e09538cc6f7542f", "keys-and-oauth/device-grant-revoked": "91df5a66c247a7fd", "keys-and-oauth/device-spent": "435afb775e386e9b", "keys-and-oauth/device-approval-catch-up": "faacc661786a5715", "keys-and-oauth/device-older-reads": "b194977803165f96", - "keys-and-oauth/device-recent-needed": "74c0c63503cef6ee", - "keys-and-oauth/device-recent-browser": "17142549e3cc5358", + "keys-and-oauth/device-recent-needed": "3154fa22a7448c22", "keys-and-oauth/device-recent-held": "ae12ff5b37d26f43", - "keys-and-oauth/device-page-recent": "b51b02c643d67b13", + "keys-and-oauth/device-page-confirm": "3605efc1f4a674a5", + "keys-and-oauth/device-page-reads": "1a7ae84abbb52750", "keys-and-oauth/device-client-gone": "f427bbbd2208b45e", "keys-and-oauth/browser-origin": "b6e56ea3b2467498", "keys-and-oauth/browser-no-origin": "2318dc8dd4795511", @@ -3617,6 +3696,8 @@ "keys-and-oauth/device-lookup-address-limit": "709a869d7675f2b8", "keys-and-oauth/device-lookup-wait": "3ac872da0a71821a", "keys-and-oauth/device-lookup-page": "516e2ef272bde7d5", + "keys-and-oauth/device-code-not-right": "31fd2c67214bc1b2", + "keys-and-oauth/device-code-lifetime": "27064a4319e60019", "keys-and-oauth/device-lookup-instance-limit": "8c44bf187c9f61dd", "keys-and-oauth/device-lookup-owner-lane": "72a04c00ed59852c", "keys-and-oauth/device-lookup-owner-limit": "b10e397702891606", @@ -3631,8 +3712,8 @@ "keys-and-oauth/caution-consent": "74bcd1827f9debdf", "keys-and-oauth/caution-device": "49599eba707d5321", "keys-and-oauth/caution-secret": "68ac7748d394c72d", - "keys-and-oauth/consent-names-page": "cc544c9aa9b617f9", - "keys-and-oauth/device-names-page": "0919d0bbf535cc29", + "keys-and-oauth/consent-names-page": "8cf40f2e2fc80c7b", + "keys-and-oauth/device-names-page": "2c23aead9c2976b2", "keys-and-oauth/sign-in-email-kept": "1dca3cdfa8dd739b", "keys-and-oauth/sign-in-email-shown": "e81c2eb1de0e831d", "keys-and-oauth/sign-in-no-password": "46cbbf189ff0a005", @@ -3641,7 +3722,7 @@ "keys-and-oauth/sign-in-unsigned-no-app": "6b1555f2c4a44caa", "keys-and-oauth/signed-in-page": "6035607b37be1066", "keys-and-oauth/signed-in-reauth": "e46308d36743c85f", - "keys-and-oauth/sign-in-page-reprompt": "692d9c5e7ab03c4b", + "keys-and-oauth/sign-in-page-reprompt": "62ec48ab4fa83b90", "keys-and-oauth/link-unverified": "ffbe8f6578572752", "keys-and-oauth/link-expired": "c36d89321ee285b4", "keys-and-oauth/end-session-page": "ef45e6c78da0e295", @@ -3677,7 +3758,8 @@ "keys-and-oauth/sign-in-end-versions": "3315a4b75f6ab456", "keys-and-oauth/sign-in-end-cookie": "3fcaeff6cc31b7fd", "keys-and-oauth/sign-in-end-unknown": "4ae2e20ebec49709", - "keys-and-oauth/sign-in-end-recent": "a5594142a3b5270e", + "keys-and-oauth/sign-in-end-recent": "0b2562bd301c0892", + "keys-and-oauth/sign-in-end-self": "d3166f19618d55c8", "keys-and-oauth/sign-in-end-restart": "27a7d1dc3761f721", "keys-and-oauth/sign-in-rename": "bfbae9d1b8d9aaa8", "keys-and-oauth/sign-in-rename-listed": "f2b2c8594fbace97", @@ -3880,7 +3962,7 @@ "queue-and-verdicts/report-unsent-verdicts": "2346206835baca0c", "queue-and-verdicts/report-held-unlisted": "a598d0f5ef4e7d47", "queue-and-verdicts/report-agrees-with-queue": "a8ea4a225d3b8c51", - "queue-and-verdicts/count-answered": "e9194f71513f7be2", + "queue-and-verdicts/count-answered": "8532e99c3f3ada18", "queue-and-verdicts/count-held": "7496eb38f49078fd", "queue-and-verdicts/count-undelivered": "2e586f9c91f246d2", "queue-and-verdicts/count-unsent": "cbe299281910dc0f", @@ -3905,7 +3987,8 @@ "queue-and-verdicts/later-row-kept": "17c212172589a022", "queue-and-verdicts/sibling-from-stream": "ef58369cfa1cceba", "queue-and-verdicts/sibling-link-held": "2c8bec46d5afda2f", - "queue-and-verdicts/refused-contract": "f35bbd75b1612c6f", + "queue-and-verdicts/refused-contract": "9838ec9469d42b47", + "queue-and-verdicts/not-retained-accepted": "6bc8270b9760adfd", "queue-and-verdicts/refused-not-resent": "3ce1a667cc64c2cc", "queue-and-verdicts/refused-envelope-kept": "f69ae215fc4e8928", "queue-and-verdicts/refused-read-again": "2b5ac4ad0f290d19", @@ -3917,9 +4000,9 @@ "queue-and-verdicts/refusal-same-in-queue": "d1e5d602c6bfd510", "queue-and-verdicts/refusal-part-absent": "976b460e4d687a55", "queue-and-verdicts/refusal-drain-made": "5404dd52ccd3ebed", - "queue-and-verdicts/blocked-reasons-five": "30a5878fe26a852f", + "queue-and-verdicts/blocked-reasons-five": "188037b16304aabd", "queue-and-verdicts/blocked-not-counted": "591c29d26eba28d3", - "queue-and-verdicts/blocked-passed-over": "3411061ff7ca5e55", + "queue-and-verdicts/blocked-passed-over": "d3a20835cb75cae4", "queue-and-verdicts/blocked-retried-itself": "2f0aed453d9387a8", "queue-and-verdicts/blocked-keeps-row": "1a59c41036791b99", "queue-and-verdicts/ceiling-five": "10383ce29ecce6cd", @@ -3929,11 +4012,30 @@ "queue-and-verdicts/environmental-uncounted": "6c2641acebc02083", "queue-and-verdicts/unnamed-refusal-environmental": "e7901bd35fac883f", "queue-and-verdicts/unnamed-refusal-said": "e3521c8ee83e704b", - "queue-and-verdicts/environmental-ends-pass": "cf1571b51b75e102", + "queue-and-verdicts/environmental-ends-pass": "ddb3925e5a332d84", "queue-and-verdicts/read-failure-ends-pass": "19d9e9111e866144", + "queue-and-verdicts/streak-counted": "c93b9d479e7a9adf", + "queue-and-verdicts/streak-run-restarts": "87c234df5075a5e9", + "queue-and-verdicts/streak-cleared": "4b6bc5344ff00749", + "queue-and-verdicts/stuck-write-suspect": "700f2eaf99f2e24b", + "queue-and-verdicts/stuck-write-tested": "cef1fa11b4864704", + "queue-and-verdicts/stuck-write-test-unsent": "c740a4efd6e46d79", + "queue-and-verdicts/stuck-write-test-unmade": "341e0519a67f3f8b", + "queue-and-verdicts/stuck-write-resent": "3feffb64a32e1d33", + "queue-and-verdicts/stuck-write-set-aside": "683e59f651bfa6dc", + "queue-and-verdicts/stuck-write-resend-answered": "efd1067029dd5bd7", + "queue-and-verdicts/stuck-write-carries-on": "11927ff273ccb44a", + "queue-and-verdicts/stuck-write-short-streak": "d2c5089946fea40e", + "queue-and-verdicts/stuck-write-sent-only": "cdc228961f335da6", + "queue-and-verdicts/stuck-write-test-failed": "4fcbd1a767e261bb", + "queue-and-verdicts/stuck-write-test-refused": "fee9ed40f77a57a8", + "queue-and-verdicts/stuck-write-alone": "229f61ed3ee2b544", + "queue-and-verdicts/failing-alone-holds-dependants": "e9c6f2fa76882a4a", + "queue-and-verdicts/failing-alone-withdraw-dependants": "cdc8584bd4f67086", + "queue-and-verdicts/queue-shows-last-failure": "a6fc251d12a2a078", "queue-and-verdicts/counted-failures": "af0528aa36dd78c5", "queue-and-verdicts/in-flight-counted": "a0671aece9c505c9", - "queue-and-verdicts/counted-goes-on": "9cf126f8a7d7df4a", + "queue-and-verdicts/counted-goes-on": "aecff034913354a8", "queue-and-verdicts/store-failure-counted": "e98159256c5fd867", "queue-and-verdicts/store-full-ends-drain": "3f82f185cc5809fe", "queue-and-verdicts/credential-blocks-queue": "79a5077d31908ba3", @@ -3961,6 +4063,7 @@ "queue-and-verdicts/release-stopped": "18e39dd28ecad49f", "queue-and-verdicts/release-fresh-key": "d6329435b8915867", "queue-and-verdicts/release-resets-count": "af249ecef875d4da", + "queue-and-verdicts/release-clears-streak": "7c9a0d04bf02975d", "queue-and-verdicts/release-by-reason": "6d799737ed27f7e5", "queue-and-verdicts/release-reason-held": "7389f6fd884a91fa", "queue-and-verdicts/release-held-by-id": "1b0b22b0c616abac", @@ -3968,9 +4071,9 @@ "queue-and-verdicts/release-takes-dependants": "24f08404533bb90e", "queue-and-verdicts/release-sent-refused": "64420d0e370784f8", "queue-and-verdicts/release-lays-over": "99fc2b18ad9c50e4", - "queue-and-verdicts/withdraw-two-reasons": "fee65d1c4eb19cd2", + "queue-and-verdicts/withdraw-two-reasons": "b0ed47449c5a31b7", "queue-and-verdicts/withdraw-unknown": "101d0fbda6c97bc2", - "queue-and-verdicts/withdraw-takes-out": "3477857d4a416473", + "queue-and-verdicts/withdraw-takes-out": "07b79bd1469a59b5", "queue-and-verdicts/withdraw-puts-back": "9ea1f7ae83411e05", "queue-and-verdicts/withdraw-read-first": "f3db5cabfb9cf530", "queue-and-verdicts/withdraw-refuses-held": "fabf78c907ff2c9e", @@ -4651,6 +4754,11 @@ "types/format-string-only-refused": "47f4bcaea2e397cc", "types/datetime-value": "fd07bee460e3d65e", "types/datetime-refused": "02b692f30451cc2f", + "types/url-value": "512fddb8c5b3e4ff", + "types/url-punycode": "1137cb9a93c3b47c", + "types/url-idna": "0eccb0c75d7f7bf3", + "types/url-host-refused": "1a5c9bf6f3bc551b", + "types/url-stored": "dd3950dd59da8624", "types/array-items": "8001e3dc5fa40455", "types/array-items-refused": "12f194f18218820c", "types/array-items-unnamed": "86a4fe72b9e76609", diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index 6b9ec6e1..449e391d 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.17"} +{"version":"v0.0.18"} diff --git a/src/generated/server-image.md b/src/generated/server-image.md index 3f5c0728..4f79b126 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.17 +docker pull ghcr.io/withmarfa/marfa-server:0.0.18 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.17 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.18 ```