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"; /// 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. Archiving anyway would hide vaults from every member including /// the ones holding keys to them, and this product has no way to delete a vault, so the refusal is /// the honest end of that road rather than a step on it. /// public const string TeamNotEmpty = "team-not-empty"; /// /// An invitation was rejected: a malformed address, an unknown or ownership role, an expiry the /// server will not issue, or an address that already has an account here. /// /// /// Separate from because the most common cause has its own remedy that a /// client can act on — an address that already has an account should be added through the /// directory instead, which is the path that shows the caller the key they are about to trust. /// public const string InvalidTeamInvitation = "invalid-team-invitation"; /// /// 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"; }