KitError taxonomy
The SDK's frozen 10-code error model — typed KitError throws, an exhaustive type guard, and the mapping from codes to styled UI copy.
One frozen 10-code taxonomy drives every failure path — and the single styled error layout, with copy swapped by code. Every throw in the SDK is a typed KitError, never a bare string.
KitError shape
KitError extends Error with a typed, exhaustive code. The 10 codes live in the frozen KIT_ERROR_CODES tuple, and isKitError is the type guard. All three come from @soropass/core/types (≈182 B gzipped).
import { KitError, KIT_ERROR_CODES, isKitError } from '@soropass/core/types';
import type { KitErrorCode } from '@soropass/core/types';
class KitError extends Error {
readonly code: KitErrorCode; // one of KIT_ERROR_CODES
readonly cause?: unknown; // the underlying error, when wrapped
}Guard pattern
import { isKitError } from '@soropass/core/types';
if (isKitError(err)) {
switch (err.code) {
case 'USER_CANCELLED': return retry();
case 'NETWORK_ERROR': return retry();
// …exhaustive over KIT_ERROR_CODES
}
}The taxonomy is frozen with Object.freeze — KIT_ERROR_CODES is exhaustively switchable, so the compiler catches a missing case when you add handling.
The 10 codes
| Code | When thrown | Thrown by | Styled UI copy | Recovery |
|---|---|---|---|---|
USER_CANCELLED | User dismissed the OS passkey sheet | assertUserActivation / ceremony | You closed the passkey prompt before it finished. | Try again |
UNSUPPORTED_AUTHENTICATOR | Device/passkey can't be used | create / sign / recover (fallback) | This device or passkey can't be used — try another. | Try again |
ES256_NOT_SUPPORTED | Non-P256 key at creation (alg ≠ −7) | assertES256 / coseKeyToSec1 | This passkey isn't supported for on-chain accounts. | Try again |
INVALID_PUBLIC_KEY | COSE key couldn't be read | coseKeyToSec1 / extractPublicKey* | We couldn't read the key from this passkey. | Try again |
INVALID_SIGNATURE_DER | Malformed / >72-byte DER signature | derToCompact | There was a problem with the signature. | Try again |
RP_ID_MISMATCH | rpIdHash ≠ expected origin | verifyRpIdHash | Couldn't verify this request — it may have changed. | Try again |
ORIGIN_MISMATCH | clientDataJSON origin mismatch | verifyClientDataJSON | Couldn't verify this request — it may have changed. | Try again |
CHALLENGE_MISMATCH | Challenge changed or expired | verifyClientDataJSON | Couldn't verify this request — it may have expired. | Try again |
CONTRACT_AUTH_FAILED | __check_auth rejected on-chain | signTransaction / deploy | Couldn't set up / authorize the account. | Try again |
NETWORK_ERROR | RPC / network unreachable | submission / indexer | We couldn't reach the network. | Retry |
From code to styled copy
The headless layer surfaces a screen-agnostic KitErrorCode; the styled layer's error connector maps (screen, code) → a screen-scoped copy key (e.g. create:cancelled, sign:verify) so the same 10 codes read naturally on each screen. One layout, copy swapped by code — see it live on the Sign and Create state galleries.
Unknown or unmapped codes fall back to the screen's *:unsupported message — a safe, device-level line — never a blank or a stack trace.