Core SDK reference
Complete reference for Ownfold domain values, format parsers, record validation, state unions, capability checks, encodings, and extension contracts.
@ownfold/core is the universal, side-effect-free contract package. It contains no runtime crypto,
storage, network client, framework adapter, or UI. Import it when application code needs to parse an
Ownfold value, implement an adapter, inspect a state, or branch on a typed error.
pnpm add @ownfold/core@beta better-result
Import map
| Category | Public exports |
|---|---|
| Encoding | Base64Url, Utf8 |
| Record validation | EncryptedRecordWrite, EncryptedRecordExpectation, EncryptedRecordWriteValue, EncryptedRecordWriteError |
| Persisted formats | EncryptedEnvelope, RecoveryKit, DeviceEnvelope, PairingOffer, Rotation |
| Stored and remote data | VaultMetadataData, VaultRecordData, DeviceData, PairingData, RemoteVaultInput, RemoteRotationInput |
| States | VaultState, StableVaultState, RotationState, RotationRecord |
| Contracts | CryptoEngine, VaultAdapter, VaultRotationAdapter, VaultTransport, VaultRotationTransport and their inputs |
| Compatibility | PersistedCryptoCapabilities, PersistedFormatSupport, PersistedCryptoFormat |
| Errors | All tagged error classes, OwnfoldError, and CryptoError |
Encoding utilities
Base64Url
Base64Url.encode(bytes) returns unpadded RFC 4648 URL-safe text. decode(value) rejects empty,
padded, or non-alphabet input with InvalidInputError code INVALID_BASE64URL.
import { Base64Url } from "@ownfold/core"
const encoded = Base64Url.encode(randomBytes)
const decoded = Base64Url.decode(encoded)
if (decoded.status === "error") return decoded
Do not use permissive platform decoders for persisted Ownfold fields. Strict decoding prevents two text representations from becoming the same authenticated byte string.
Utf8
Utf8.encode(value) returns UTF-8 bytes. Utf8.decode(bytes) uses fatal decoding and returns
INVALID_UTF8 instead of inserting replacement characters.
Record context and envelope values
namespacestring
stringrecordIdstring
stringownerId?string
stringnamespace, recordId, and ownerId are authenticated record context. They must come from stable
application identifiers. Moving ciphertext to a different value for any field makes decryption
fail.
format"ownfold.encrypted-envelope"
"ownfold.encrypted-envelope"version2
2suite"XCHACHA20-POLY1305-IETF"
"XCHACHA20-POLY1305-IETF"vaultIdstring
stringkeyVersionnumber
numbercontextRecordContext
RecordContextcontent{
readonly encoding: "json" | "binary"
readonly nonce: string
readonly ciphertext: string
}
{
readonly encoding: "json" | "binary"
readonly nonce: string
readonly ciphertext: string
}wrappedDataKey{
readonly nonce: string
readonly ciphertext: string
}
{
readonly nonce: string
readonly ciphertext: string
}EncryptedEnvelopeValue accepts supported persisted versions. New writes use the current engine
default; readers may support older versions during migration. Never construct envelopes manually.
Parsing persisted values
Every parser accepts unknown and returns Result<T, OwnfoldError>.
| Namespace | Methods | Input boundary |
|---|---|---|
EncryptedEnvelope |
parse, parseString, stringify |
Application ciphertext columns and API responses |
RecoveryKit |
parse, parseString, stringify |
User-selected Recovery Kit file contents |
DeviceEnvelope |
parse, stringify |
Persisted or remote device envelopes |
PairingOffer |
parse, parseString, stringify |
Shared pairing text or QR payloads |
Rotation |
parseState, parseRecord |
Resumable rotation metadata |
VaultMetadataData |
parse |
Remote-safe vault metadata |
VaultRecordData |
parse |
Adapter-owned vault rows including userId |
DeviceData |
parseSummary, parseRecord |
Remote device views or stored device rows |
PairingData |
parseRequest, parseRecord |
Remote or stored pairing state |
import { EncryptedEnvelope } from "@ownfold/core"
const parsed = EncryptedEnvelope.parse(databaseRow.encryptedPayload)
if (parsed.status === "error") {
// Preserve the original row. Do not return a partially parsed envelope.
return parsed
}
return parsed.value
The exported *Schema values are Standard Schema-compatible Valibot schemas used by the namespace
parsers. Prefer the namespace parser when it exists because it maps validation failures to stable
Ownfold errors and enforces supported versions.
EncryptedRecordWrite
Use this lower-level validator when an application record route is not calling
VaultServer.validateEncryptedRecordWrite().
vaultIdstring
stringkeyVersionnumber
numbercontextRecordContext
RecordContextimport { EncryptedRecordWrite } from "@ownfold/core"
const parsed = EncryptedRecordWrite.parse(body, {
vaultId: activeVault.vaultId,
keyVersion: activeVault.keyVersion,
context: {
namespace: "documents",
recordId: documentId,
ownerId: authenticatedUserId,
},
})
if (parsed.status === "error") return parsed
await documents.save(parsed.value.encryptedPayload)
The input object must contain exactly one field, encryptedPayload. Sibling plaintext-shaped fields
are rejected. The validator checks structure and authenticated metadata; only a client holding the
root key can verify the AEAD tag.
Vault state
status"not-created"
"not-created"Always switch on state.status. Do not infer readiness from loosely optional fields.
| Status | Key access | Meaning |
|---|---|---|
not-created |
No | No local or remote vault exists. |
draft-unprotected |
In-memory only | Root key exists but no Recovery Kit protects it. |
sync-pending |
Local pending state | Remote creation must be resumed. |
recovery-unverified |
Controlled onboarding | Downloaded kit must be re-imported and verified. |
ready-locked |
No | Device is enrolled but key handle is destroyed. |
ready-unlocked |
Yes | Record operations are available. |
unavailable-on-device |
No | Remote vault exists without a local enrolled identity. |
pairing-pending |
No | New device is awaiting approval. |
operation-error |
Same as previous |
An expected operation failed; stable prior state is retained. |
StableVaultState excludes transient setup states. operation-error.previous is the state to return
to after the application presents or dismisses the error.
Rotation state
rotationIdstring
stringvaultIdstring
stringfromKeyVersionnumber
numbertoKeyVersionnumber
numberstartedAtstring
stringupdatedAtstring
stringprocessedRecordsnumber
numbercheckpoint?string
stringrevisionnumber
numberstatus"in-progress"
"in-progress"Rotation is a resumable discriminated union. The active variants retain the rotation ID, source and
target key versions, revision, processed-record count, and optional checkpoint. Only Rotation
parsers should read persisted rotation JSON.
Crypto contract
createRootKey() => Promise<Result$1<RootKeyHandle, CryptoEngineError>>
() => Promise<Result$1<RootKeyHandle, CryptoEngineError>>encryptRecord(input: EncryptRecordInput) => Promise<Result$1<EncryptedEnvelopeValue, CryptoError>>
(input: EncryptRecordInput) => Promise<Result$1<EncryptedEnvelopeValue, CryptoError>>decryptRecord(input: DecryptRecordInput) => Promise<Result$1<Uint8Array<ArrayBufferLike>, CryptoError>>
(input: DecryptRecordInput) => Promise<Result$1<Uint8Array<ArrayBufferLike>, CryptoError>>rewrapRecordDataKey(input: RewrapRecordDataKeyInput) => Promise<Result$1<EncryptedEnvelopeV2, CryptoError>>
(input: RewrapRecordDataKeyInput) => Promise<Result$1<EncryptedEnvelopeV2, CryptoError>>createRecoveryKit(input: CreateRecoveryKitInput) => Promise<Result$1<RecoveryKitV1, CryptoError>>
(input: CreateRecoveryKitInput) => Promise<Result$1<RecoveryKitV1, CryptoError>>openRecoveryKit(input: OpenRecoveryKitInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
(input: OpenRecoveryKitInput) => Promise<Result$1<RootKeyHandle, CryptoError>>createDeviceKey() => Promise<Result$1<DeviceKeyHandle, CryptoEngineError>>
() => Promise<Result$1<DeviceKeyHandle, CryptoEngineError>>wrapRootKeyForDevice(input: WrapRootKeyForDeviceInput) => Promise<Result$1<DeviceEnvelopeV1, CryptoError>>
(input: WrapRootKeyForDeviceInput) => Promise<Result$1<DeviceEnvelopeV1, CryptoError>>openDeviceEnvelope(input: OpenDeviceEnvelopeInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
(input: OpenDeviceEnvelopeInput) => Promise<Result$1<RootKeyHandle, CryptoError>>compareRootKeys(left: RootKeyHandle, right: RootKeyHandle) => Promise<Result$1<boolean, CryptoEngineError>>
(left: RootKeyHandle, right: RootKeyHandle) => Promise<Result$1<boolean, CryptoEngineError>>destroyRootKey(key: RootKeyHandle) => void
(key: RootKeyHandle) => voidRootKeyHandle and DeviceKeyHandle are opaque capabilities. Callers may pass them back to the
engine but cannot read key bytes. destroyRootKey() must make further use fail.
| Method | Result | Important invariant |
|---|---|---|
createRootKey |
Opaque root handle | Uses a cryptographically secure random source. |
encryptRecord |
Versioned envelope | Authenticates vault, key version, encoding, and record context. |
decryptRecord |
Plain bytes | Returns nothing on context or authentication failure. |
rewrapRecordDataKey |
V2 envelope | Rewraps the data key without decrypting record plaintext. |
| Recovery methods | Kit or root handle | Secret and plaintext key remain local. |
| Device methods | Key handle or envelope | Private device key never crosses the engine boundary. |
The production implementation is documented under Crypto engine. Custom engines must return the exact typed errors and preserve the same authenticated formats.
Storage and transport contracts
getVault(input: { readonly userId: string; }) => Promise<Result$1<VaultRecord | null, StorageAdapterError>>
(input: { readonly userId: string; }) => Promise<Result$1<VaultRecord | null, StorageAdapterError>>createVault(input: CreateVaultInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
(input: CreateVaultInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>updateRecoveryStatus(input: UpdateRecoveryStatusInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
(input: UpdateRecoveryStatusInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>replaceRecoveryKit(input: ReplaceRecoveryKitInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
(input: ReplaceRecoveryKitInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>listDevices(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<readonly DeviceRecord[], StorageAdapterError>>
(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<readonly DeviceRecord[], StorageAdapterError>>registerDevice(input: RegisterDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
(input: RegisterDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>touchDevice(input: TouchDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
(input: TouchDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>revokeDevice(input: RevokeDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
(input: RevokeDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>createPairing(input: CreatePairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>
(input: CreatePairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>getPairing(input: { readonly userId: string; readonly requestId: string; }) => Promise<Result$1<PairingRecord | null, StorageAdapterError>>
(input: { readonly userId: string; readonly requestId: string; }) => Promise<Result$1<PairingRecord | null, StorageAdapterError>>approvePairing(input: ApprovePairingInput) => Promise<Result$1<ApprovedPairing, StorageAdapterError | ConflictError>>
(input: ApprovePairingInput) => Promise<Result$1<ApprovedPairing, StorageAdapterError | ConflictError>>cancelPairing(input: CancelPairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>
(input: CancelPairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>getRotation(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<RotationRecord | null, StorageAdapterError>>
(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<RotationRecord | null, StorageAdapterError>>beginRotation(input: BeginRotationInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>
(input: BeginRotationInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>updateRotationProgress(input: UpdateRotationProgressInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>
(input: UpdateRotationProgressInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>completeRotation(input: CompleteRotationInput) => Promise<Result$1<CompletedRotation, StorageAdapterError | ConflictError>>
(input: CompleteRotationInput) => Promise<Result$1<CompletedRotation, StorageAdapterError | ConflictError>>getVault() => Promise<Result$1<VaultMetadata | null, TransportError>>
() => Promise<Result$1<VaultMetadata | null, TransportError>>createVault(input: CreateRemoteVaultInput) => Promise<Result$1<VaultMetadata, TransportError>>
(input: CreateRemoteVaultInput) => Promise<Result$1<VaultMetadata, TransportError>>markRecoveryVerified(input: MarkRecoveryVerifiedInput) => Promise<Result$1<VaultMetadata, TransportError>>
(input: MarkRecoveryVerifiedInput) => Promise<Result$1<VaultMetadata, TransportError>>replaceRecoveryKit(input: ReplaceRemoteRecoveryKitInput) => Promise<Result$1<VaultMetadata, TransportError>>
(input: ReplaceRemoteRecoveryKitInput) => Promise<Result$1<VaultMetadata, TransportError>>listDevices() => Promise<Result$1<readonly DeviceSummary[], TransportError>>
() => Promise<Result$1<readonly DeviceSummary[], TransportError>>registerDevice(input: RegisterRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
(input: RegisterRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>touchDevice(input: TouchRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
(input: TouchRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>revokeDevice(input: RevokeRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
(input: RevokeRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>createPairing(input: CreatePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
(input: CreatePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>getPairing(input: GetPairingRequestInput) => Promise<Result$1<PairingRequest | null, TransportError>>
(input: GetPairingRequestInput) => Promise<Result$1<PairingRequest | null, TransportError>>approvePairing(input: ApprovePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
(input: ApprovePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>cancelPairing(input: CancelPairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
(input: CancelPairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>getRotation() => Promise<Result$1<RotationState | null, TransportError>>
() => Promise<Result$1<RotationState | null, TransportError>>beginRotation(input: BeginRemoteRotationInput) => Promise<Result$1<RotationState, TransportError>>
(input: BeginRemoteRotationInput) => Promise<Result$1<RotationState, TransportError>>updateRotationProgress(input: UpdateRemoteRotationProgressInput) => Promise<Result$1<RotationState, TransportError>>
(input: UpdateRemoteRotationProgressInput) => Promise<Result$1<RotationState, TransportError>>completeRotation(input: CompleteRemoteRotationInput) => Promise<Result$1<CompletedRemoteRotation, TransportError>>
(input: CompleteRemoteRotationInput) => Promise<Result$1<CompletedRemoteRotation, TransportError>>The method shapes are documented here; transaction, ownership, idempotency, and retry semantics are specified in Adapter and transport contracts.
Remote input schemas
RemoteVaultInput and RemoteRotationInput group strict parsers for every untrusted lifecycle
request. Individual schema exports are available for framework integrations.
| Operation family | Schema exports |
|---|---|
| Vault | CreateRemoteVaultInputSchema, MarkRecoveryVerifiedInputSchema, ReplaceRemoteRecoveryKitInputSchema |
| Device | RegisterRemoteDeviceInputSchema, TouchRemoteDeviceInputSchema, RevokeRemoteDeviceInputSchema |
| Pairing | CreatePairingRequestInputSchema, GetPairingRequestInputSchema, ApprovePairingRequestInputSchema, CancelPairingRequestInputSchema |
| Rotation | BeginRemoteRotationInputSchema, UpdateRemoteRotationProgressInputSchema, CompleteRemoteRotationInputSchema |
Do not cast request.json() to these interfaces. Parse it through the namespace or schema and keep
validation at the transport boundary.
Capability and format support
PersistedCryptoCapabilities.parse(input) validates a capability manifest. parseString handles
JSON text, stringify emits the exchange form, and checkCompatibility(local, remote) returns:
compatible— every persisted format is readable;incompatible— one side cannot read the suite the other side writes, with the exact format and directional compatibility fields.
PersistedFormatSupport is the release manifest of readable versions and the default write version
for device envelopes, encrypted envelopes, pairing offers, protected device keys, and Recovery
Kits. Persisted format identifiers come from PersistedCryptoFormat; do not use package versions as
format versions.
Errors and results
Expected failures use Result from better-result; methods do not throw for invalid input, locked
state, conflicts, or authentication failure.
const result = await operation()
if (result.status === "error") {
console.error(result.error.code) // safe machine-readable field
return
}
use(result.value)
See Errors for every tagged family and recovery behavior, and the complete export index for every public type and schema name.