Error codes
Every failure the SDK raises is a SatchelError with a code, a cause in the message, a fix and a docs path to its entry here. The fix text on this page is read from the SDK build, not retyped, and the summary table and the entries below are drawn from the same rows.
| Code | Thrown by |
|---|
PRF_UNAVAILABLE | open, enrol, openOrEnrol, fromPrfOutput, Satchel.addPasskey |
PASSKEY_CANCELLED | open, enrol, openOrEnrol, Satchel.addPasskey |
PASSKEY_NOT_FOUND | openOrEnrol |
CHOOSE_PASSKEY_MODE | openOrEnrol |
RPID_MISMATCH | open, enrol, openOrEnrol, Satchel.addPasskey |
WRONG_PASSKEY | open, openOrEnrol, Satchel.unseal, Satchel.unsealJson |
NAMESPACE_MISMATCH | Satchel.unseal, Satchel.unsealJson, fetchGrant, Reader.unseal, Reader.unsealJson |
SEAL_CORRUPT | Satchel.unseal, Satchel.unsealJson, parseSealed, Reader.unseal, Reader.unsealJson |
GRANT_REVOKED | Reader.unseal, Reader.unsealJson |
GRANT_NOT_FOR_KEY | Reader.unseal, Reader.unsealJson |
REGISTRY_UNREACHABLE | Satchel.seal, Satchel.grant, Satchel.regrant, Satchel.revoke, fetchGrant, epochOf |
NO_SECOND_PASSKEY | open, openOrEnrol |
INVALID_NAMESPACE | ns |
NOT_SEALED | Satchel.unseal, Satchel.unsealJson, parseSealed, Reader.unseal, Reader.unsealJson |
SESSION_CLOSED | Satchel.seal, Satchel.unseal, Satchel.unsealJson, Satchel.fingerprint, Satchel.addPasskey, Satchel.grant, Satchel.regrant, Satchel.revoke |
GRANT_EPOCH_OLDER | Reader.unseal, Reader.unsealJson |
INVALID_GRANTEE_KEY | Satchel.grant, Satchel.regrant |
STORE_UNAVAILABLE | open, openOrEnrol, rememberedPasskey, Satchel.addPasskey |
OWNER_UNFUNDED | Satchel.grant, Satchel.regrant, Satchel.revoke |
PRF_UNAVAILABLE
The authenticator returned no 32 byte PRF output, so there is nothing to derive keys from. Some password managers and security keys do not implement the WebAuthn PRF extension.
Fix. use iCloud Keychain, Google Password Manager, Windows Hello, 1Password, Proton Pass or a YubiKey 5.
Thrown by open, enrol, openOrEnrol, fromPrfOutput, Satchel.addPasskey.
PASSKEY_CANCELLED
The passkey prompt was closed or timed out. open() and enrol() raise it because WebAuthn gives the page the same answer for a closed prompt and for a device with no matching passkey; openOrEnrol() turns that answer into PASSKEY_NOT_FOUND whenever it was opening, and raises this only for a closed create prompt.
Fix. call the same function again from a click handler and confirm the passkey prompt.
Thrown by open, enrol, openOrEnrol, Satchel.addPasskey.
PASSKEY_NOT_FOUND
openOrEnrol() was opening a passkey and none was chosen: the prompt was closed, or the passkey this device remembered was deleted, lives on another device, or belonged to an authenticator that is gone. When a remembered passkey fails, openOrEnrol() deletes that memory first, so the next call is a first visit and nothing is stuck. WebAuthn cannot tell a closed prompt from a missing passkey, so both end here. Nothing was created.
Fix. offer both choices again: open() or openOrEnrol({ mode: "open" }) to pick another passkey, enrol() or openOrEnrol({ mode: "enrol" }) to create one. The passkey this device remembered is forgotten, so nothing blocks either choice.
Thrown by openOrEnrol.
CHOOSE_PASSKEY_MODE
openOrEnrol() found no remembered passkey and was given no mode of "open" or "enrol". It stops before any prompt, because a first visit that tried to open and then create would chain two WebAuthn ceremonies from one click, which browsers can refuse and which can duplicate a passkey.
Fix. pass mode: "enrol" to create a passkey or mode: "open" to use one the person already has, each from its own click handler. openOrEnrol() runs one passkey prompt per call and never chains both.
Thrown by openOrEnrol.
RPID_MISMATCH
The page origin is not the rpId the passkey was created under, for example a preview URL. A passkey works only under its own rpId.
Fix. pin rpId to the hostname the passkey was created under, usually your production hostname.
Thrown by open, enrol, openOrEnrol, Satchel.addPasskey.
WRONG_PASSKEY
The passkey's output derives a different key than the one that sealed the data, or the stored wrap for an added passkey failed authentication.
Fix. open with the passkey that sealed this data, or use a second passkey added with addPasskey().
Thrown by open, openOrEnrol, Satchel.unseal, Satchel.unsealJson.
NAMESPACE_MISMATCH
The blob header's namespace hash differs from the namespace you asked for, or a grant fetched with fetchGrant() covers a different namespace hash than the one named. Raised before any decryption.
Fix. unseal with the namespace the blob was sealed under.
Thrown by Satchel.unseal, Satchel.unsealJson, fetchGrant, Reader.unseal, Reader.unsealJson.
SEAL_CORRUPT
The blob is shorter than its header plus one GCM tag, or its ciphertext failed authentication under a grant's key. It was altered or truncated and nothing was decrypted.
Fix. re-fetch the blob from where it was stored; this copy is truncated or altered.
Thrown by Satchel.unseal, Satchel.unsealJson, parseSealed, Reader.unseal, Reader.unsealJson.
SEAL_CORRUPT The blob is truncated: it is shorter than its header plus one GCM tag.
fix: re-fetch the blob from where it was stored; this copy is truncated or altered.
docs: /reference/errors#seal-corrupt
GRANT_REVOKED
A reader's grant covers one epoch and the record is from a later one. The owner moved the namespace on: revoking any reader's grant moves the namespace to the next epoch for every reader, so the revoked app stays locked out and every other reader needs regrant() before it can open new records.
Fix. ask the owner to call regrant() for your public key: a grant covers one epoch only, and revoking any reader of a namespace moves every reader to the next epoch.
Thrown by Reader.unseal, Reader.unsealJson.
GRANT_NOT_FOR_KEY
The grant was wrapped to a different grantee key, or could not be opened with this app's key.
Fix. ask the owner to grant to this app's public key from createGrantee().
Thrown by Reader.unseal, Reader.unsealJson.
REGISTRY_UNREACHABLE
A registry read or write did not answer, did not confirm within 30 seconds, or reverted. Also raised when no registry is configured on the session, and by fetchGrant() when the hash it was given has no receipt, reverted, or has no Granted event from that registry.
Fix. check the registry address and RPC, then retry. Call epochOf(registry, session.registryOwner, nsHashOf(ns)) first to see whether the earlier call landed.
Thrown by Satchel.seal, Satchel.grant, Satchel.regrant, Satchel.revoke, fetchGrant, epochOf.
NO_SECOND_PASSKEY
Recovery was requested but the chosen passkey was never added to this vault with addPasskey(), or no store is available to look it up.
Fix. call addPasskey() while the original passkey is available.
Thrown by open, openOrEnrol.
INVALID_NAMESPACE
The path is not 3 to 64 lowercase letters and digits joined by / or -.
Fix. use lowercase letters and digits joined by / or -, 3 to 64 characters, e.g. memory/assistant.
Thrown by ns.
INVALID_NAMESPACE "Memory/Assistant" is not a namespace. A namespace is 3 to 64 characters matching /^[a-z0-9]+([/-][a-z0-9]+)*$/.
fix: use lowercase letters and digits joined by / or -, 3 to 64 characters, e.g. memory/assistant.
docs: /reference/errors#invalid-namespace
NOT_SEALED
The bytes are not a satchel blob: the SAT1 header is missing, the version is unsupported, or the epoch field is impossible.
Fix. pass bytes produced by seal().toBytes().
Thrown by Satchel.unseal, Satchel.unsealJson, parseSealed, Reader.unseal, Reader.unsealJson.
NOT_SEALED These bytes are not a satchel blob: the SAT1 header is missing.
fix: pass bytes produced by seal().toBytes().
docs: /reference/errors#not-sealed
SESSION_CLOSED
close() zeroed this session's root. Any later call throws.
Fix. call open() again.
Thrown by Satchel.seal, Satchel.unseal, Satchel.unsealJson, Satchel.fingerprint, Satchel.addPasskey, Satchel.grant, Satchel.regrant, Satchel.revoke.
GRANT_EPOCH_OLDER
The record is from an epoch earlier than the grant covers. A grant cannot derive keys for epochs other than its own.
Fix. use the grant issued for the epoch this blob was sealed at.
Thrown by Reader.unseal, Reader.unsealJson.
INVALID_GRANTEE_KEY
The grantee key is not hex, is not 65 bytes in uncompressed form, or is not a point on P-256.
Fix. pass the publicKey returned by createGrantee().
Thrown by Satchel.grant, Satchel.regrant.
STORE_UNAVAILABLE
No store was passed and this runtime has no IndexedDB, or the store refused a read or write for an added passkey or for the passkey marker openOrEnrol keeps.
Fix. pass a store in OpenOptions; this runtime has no IndexedDB.
Thrown by open, openOrEnrol, rememberedPasskey, Satchel.addPasskey.
OWNER_UNFUNDED
The registry owner address, derived from the passkey, cannot pay for the transaction. grant, regrant and revoke read its balance and the gas estimate first and stop before sending, so nothing was spent and no state changed. The message names the address and the shortfall; the fix names where to send it.
Fix. send the native token (testnet MON from https://faucet.monad.xyz on Monad testnet) to session.registryOwner, then call grant() or revoke() again.
Thrown by Satchel.grant, Satchel.regrant, Satchel.revoke.