namespace DodoSSH.Contracts; /// /// Stable machine-readable error codes returned in the code extension of an /// RFC 9457 ProblemDetails response. /// /// /// These live in Contracts so the client switches on constants rather than parsing prose. /// The values are part of the public contract: add freely, never rename or repurpose. /// public static class ProblemCodes { /// The base URI that every problem type is formed under. public const string TypeBaseUri = "https://dodossh.dev/problems/"; /// A push operation's expectedVersion did not match the stored row. public const string VaultConflict = "vault-conflict"; /// The caller is authenticated but lacks the required permission. public const string Forbidden = "forbidden"; /// The sync cursor was malformed, or failed its integrity tag. public const string InvalidCursor = "invalid-cursor"; /// An Idempotency-Key was reused with a different request body. public const string IdempotencyKeyReuse = "idempotency-key-reuse"; /// The caller has not yet enrolled a public key, so no vault is reachable. public const string EnrollmentRequired = "enrollment-required"; /// Enrollment was attempted for a user who already holds a different current key. public const string AlreadyEnrolled = "already-enrolled"; /// /// The enrollment request was structurally invalid: a bad key length, mismatched KDF /// parameters, a statement that does not describe the caller, or a vault id already in use. /// public const string InvalidEnrollment = "invalid-enrollment"; /// /// The identity-provider token did not bind the supplied keys: a bad signature, the wrong /// subject or audience, an expired token, or a nonce that is not the statement's hash. /// public const string IdentityBindingInvalid = "identity-binding-invalid"; /// /// A device registration was structurally invalid: a public key of the wrong length, a missing or /// oversized wrap, or a blank name. /// /// /// Distinct from even though the rules overlap, because the two are /// different requests and a client showing "your enrollment was rejected" when somebody added a /// fingerprint reader would be describing the wrong thing entirely. /// public const string InvalidDeviceRegistration = "invalid-device-registration"; /// /// The request body could not be read at all: malformed JSON, or a property the server does not /// know. /// /// /// A contract mismatch rather than a rejected value. The request never reached a handler, so no /// field-level detail is offered and none should be inferred from its absence. Compare the request /// against the DodoSSH.Contracts assembly for the server version GET /api/v1/meta /// reports. /// public const string MalformedRequest = "malformed-request"; /// /// This deployment does not push vault changes over a socket. /// /// /// Not a failure to recover from: synchronising on a timer is the supported behaviour and the /// socket only ever made it early. A client that meets this stops dialling and keeps polling. See /// ADR 0012. /// public const string EventsUnavailable = "events-unavailable"; /// /// This deployment does not relay. /// /// /// Its own code rather than , because it is the one relay refusal /// whose remedy is not about the host: nothing the user changes on this machine will make it work /// here, and saying so is not an oracle for anything. GET /api/v1/meta says the same thing /// without asking. /// public const string RelayUnavailable = "relay-unavailable"; /// The relay refused the requested target. Never states why, to avoid a probe oracle. public const string RelayTargetRejected = "relay-target-rejected"; /// The relay ticket is expired, already used, or not valid for this node. public const string RelayTicketInvalid = "relay-ticket-invalid"; /// A per-user or per-node relay session limit was reached. public const string RelayLimitReached = "relay-limit-reached"; /// The client is older than the server's minClientVersion. public const string ClientTooOld = "client-too-old"; /// A push batch exceeded the operation count or payload size cap. public const string PushBatchTooLarge = "push-batch-too-large"; /// /// A team create or membership change was structurally invalid: a blank name, a slug that is /// not URL-safe, an unknown role, or an account that does not exist here. /// public const string InvalidTeam = "invalid-team"; /// /// The requested slug is already in use. /// /// /// Its own code rather than folded into , because it is the one create /// failure the caller could not have predicted from their own input and the only one whose /// remedy is "pick a different one" rather than "fix what you typed". /// public const string TeamSlugTaken = "team-slug-taken"; /// /// The change would leave a team with no owner. /// /// /// Refused rather than allowed, because a team with no owner has nobody who can appoint one — /// and the only route back would be an operator editing the database by hand. The way past it is /// POST /api/v1/teams/{teamId}/owner, which moves ownership and the outgoing owner's /// demotion in one transaction; a client that gets this code can offer that. /// public const string LastTeamOwner = "last-team-owner"; /// /// A team cannot be archived while it still owns vaults. /// /// /// Its own code because the remedy is neither "fix what you typed" nor "pick another value": it is to /// deal with the vaults first, by deleting each of them — DELETE /api/v1/vaults/{vaultId} — which /// asks about each one on the way. Archiving anyway would hide vaults from every member including the /// ones holding keys to them, which is the same removal without anybody having been asked. /// public const string TeamNotEmpty = "team-not-empty"; /// /// An address given to POST /api/v1/teams/{teamId}/members has no account on this server. /// /// /// /// Its own code rather than folded into because it is the one add failure /// that is not about the request: the request was fine and named somebody who is not here. A client /// that could not tell this apart from a rejected role would have to say "check what you typed" /// about an address that was typed correctly. /// /// /// Nothing follows it. This used to be the signal to invite the address instead; there are /// no invitations, and a membership is only ever granted to an account somebody named, so the whole /// of what a client can do with this is say that the person has to sign in here once. See /// . /// /// /// It answers whether an address has an account here, which is a real disclosure and a bounded one: /// only an admin or owner of the team the add names reaches it — the endpoint checks that first — /// and it is the same fact the member list would show them a moment later. /// /// public const string NoSuchAccount = "no-such-account"; /// /// A vault key grant was rejected: a fingerprint or wrap of the wrong size, a generation that is /// not the vault's current one, or a recipient who cannot reach the vault in the first place. /// /// /// Never a statement about the wrapped key's contents. The server cannot open it, so a /// grant containing garbage is accepted here and surfaces at the recipient as a tag failure, /// with the signature naming who issued it. See docs/crypto.md §6. /// public const string InvalidVaultGrant = "invalid-vault-grant"; }