Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

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

PropType
namespacestring
Typestring
recordIdstring
Typestring
ownerId?string
Typestring

namespace, 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.

PropType
format"ownfold.encrypted-envelope"
Type"ownfold.encrypted-envelope"
version2
Type2
suite"XCHACHA20-POLY1305-IETF"
Type"XCHACHA20-POLY1305-IETF"
vaultIdstring
Typestring
keyVersionnumber
Typenumber
contextRecordContext
TypeRecordContext
content{ readonly encoding: "json" | "binary" readonly nonce: string readonly ciphertext: string }
Type{ readonly encoding: "json" | "binary" readonly nonce: string readonly ciphertext: string }
wrappedDataKey{ readonly nonce: string readonly ciphertext: string }
Type{ 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().

PropType
vaultIdstring
Typestring
keyVersionnumber
Typenumber
contextRecordContext
TypeRecordContext
import { 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

PropType
status"not-created"
Type"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

PropType
rotationIdstring
Typestring
vaultIdstring
Typestring
fromKeyVersionnumber
Typenumber
toKeyVersionnumber
Typenumber
startedAtstring
Typestring
updatedAtstring
Typestring
processedRecordsnumber
Typenumber
checkpoint?string
Typestring
revisionnumber
Typenumber
status"in-progress"
Type"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

PropType
createRootKey() => Promise<Result$1<RootKeyHandle, CryptoEngineError>>
Type() => Promise<Result$1<RootKeyHandle, CryptoEngineError>>
encryptRecord(input: EncryptRecordInput) => Promise<Result$1<EncryptedEnvelopeValue, CryptoError>>
Type(input: EncryptRecordInput) => Promise<Result$1<EncryptedEnvelopeValue, CryptoError>>
decryptRecord(input: DecryptRecordInput) => Promise<Result$1<Uint8Array<ArrayBufferLike>, CryptoError>>
Type(input: DecryptRecordInput) => Promise<Result$1<Uint8Array<ArrayBufferLike>, CryptoError>>
rewrapRecordDataKey(input: RewrapRecordDataKeyInput) => Promise<Result$1<EncryptedEnvelopeV2, CryptoError>>
Type(input: RewrapRecordDataKeyInput) => Promise<Result$1<EncryptedEnvelopeV2, CryptoError>>
createRecoveryKit(input: CreateRecoveryKitInput) => Promise<Result$1<RecoveryKitV1, CryptoError>>
Type(input: CreateRecoveryKitInput) => Promise<Result$1<RecoveryKitV1, CryptoError>>
openRecoveryKit(input: OpenRecoveryKitInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
Type(input: OpenRecoveryKitInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
createDeviceKey() => Promise<Result$1<DeviceKeyHandle, CryptoEngineError>>
Type() => Promise<Result$1<DeviceKeyHandle, CryptoEngineError>>
wrapRootKeyForDevice(input: WrapRootKeyForDeviceInput) => Promise<Result$1<DeviceEnvelopeV1, CryptoError>>
Type(input: WrapRootKeyForDeviceInput) => Promise<Result$1<DeviceEnvelopeV1, CryptoError>>
openDeviceEnvelope(input: OpenDeviceEnvelopeInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
Type(input: OpenDeviceEnvelopeInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
compareRootKeys(left: RootKeyHandle, right: RootKeyHandle) => Promise<Result$1<boolean, CryptoEngineError>>
Type(left: RootKeyHandle, right: RootKeyHandle) => Promise<Result$1<boolean, CryptoEngineError>>
destroyRootKey(key: RootKeyHandle) => void
Type(key: RootKeyHandle) => void

RootKeyHandle 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

PropType
getVault(input: { readonly userId: string; }) => Promise<Result$1<VaultRecord | null, StorageAdapterError>>
Type(input: { readonly userId: string; }) => Promise<Result$1<VaultRecord | null, StorageAdapterError>>
createVault(input: CreateVaultInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
Type(input: CreateVaultInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
updateRecoveryStatus(input: UpdateRecoveryStatusInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
Type(input: UpdateRecoveryStatusInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
replaceRecoveryKit(input: ReplaceRecoveryKitInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
Type(input: ReplaceRecoveryKitInput) => Promise<Result$1<VaultRecord, StorageAdapterError | ConflictError>>
listDevices(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<readonly DeviceRecord[], StorageAdapterError>>
Type(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<readonly DeviceRecord[], StorageAdapterError>>
registerDevice(input: RegisterDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
Type(input: RegisterDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
touchDevice(input: TouchDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
Type(input: TouchDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
revokeDevice(input: RevokeDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
Type(input: RevokeDeviceInput) => Promise<Result$1<DeviceRecord, StorageAdapterError | ConflictError>>
createPairing(input: CreatePairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>
Type(input: CreatePairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>
getPairing(input: { readonly userId: string; readonly requestId: string; }) => Promise<Result$1<PairingRecord | null, StorageAdapterError>>
Type(input: { readonly userId: string; readonly requestId: string; }) => Promise<Result$1<PairingRecord | null, StorageAdapterError>>
approvePairing(input: ApprovePairingInput) => Promise<Result$1<ApprovedPairing, StorageAdapterError | ConflictError>>
Type(input: ApprovePairingInput) => Promise<Result$1<ApprovedPairing, StorageAdapterError | ConflictError>>
cancelPairing(input: CancelPairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>
Type(input: CancelPairingInput) => Promise<Result$1<PairingRecord, StorageAdapterError | ConflictError>>
PropType
getRotation(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<RotationRecord | null, StorageAdapterError>>
Type(input: { readonly userId: string; readonly vaultId: string; }) => Promise<Result$1<RotationRecord | null, StorageAdapterError>>
beginRotation(input: BeginRotationInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>
Type(input: BeginRotationInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>
updateRotationProgress(input: UpdateRotationProgressInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>
Type(input: UpdateRotationProgressInput) => Promise<Result$1<RotationRecord, StorageAdapterError | ConflictError>>
completeRotation(input: CompleteRotationInput) => Promise<Result$1<CompletedRotation, StorageAdapterError | ConflictError>>
Type(input: CompleteRotationInput) => Promise<Result$1<CompletedRotation, StorageAdapterError | ConflictError>>
PropType
getVault() => Promise<Result$1<VaultMetadata | null, TransportError>>
Type() => Promise<Result$1<VaultMetadata | null, TransportError>>
createVault(input: CreateRemoteVaultInput) => Promise<Result$1<VaultMetadata, TransportError>>
Type(input: CreateRemoteVaultInput) => Promise<Result$1<VaultMetadata, TransportError>>
markRecoveryVerified(input: MarkRecoveryVerifiedInput) => Promise<Result$1<VaultMetadata, TransportError>>
Type(input: MarkRecoveryVerifiedInput) => Promise<Result$1<VaultMetadata, TransportError>>
replaceRecoveryKit(input: ReplaceRemoteRecoveryKitInput) => Promise<Result$1<VaultMetadata, TransportError>>
Type(input: ReplaceRemoteRecoveryKitInput) => Promise<Result$1<VaultMetadata, TransportError>>
listDevices() => Promise<Result$1<readonly DeviceSummary[], TransportError>>
Type() => Promise<Result$1<readonly DeviceSummary[], TransportError>>
registerDevice(input: RegisterRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
Type(input: RegisterRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
touchDevice(input: TouchRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
Type(input: TouchRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
revokeDevice(input: RevokeRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
Type(input: RevokeRemoteDeviceInput) => Promise<Result$1<DeviceSummary, TransportError>>
createPairing(input: CreatePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
Type(input: CreatePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
getPairing(input: GetPairingRequestInput) => Promise<Result$1<PairingRequest | null, TransportError>>
Type(input: GetPairingRequestInput) => Promise<Result$1<PairingRequest | null, TransportError>>
approvePairing(input: ApprovePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
Type(input: ApprovePairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
cancelPairing(input: CancelPairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
Type(input: CancelPairingRequestInput) => Promise<Result$1<PairingRequest, TransportError>>
PropType
getRotation() => Promise<Result$1<RotationState | null, TransportError>>
Type() => Promise<Result$1<RotationState | null, TransportError>>
beginRotation(input: BeginRemoteRotationInput) => Promise<Result$1<RotationState, TransportError>>
Type(input: BeginRemoteRotationInput) => Promise<Result$1<RotationState, TransportError>>
updateRotationProgress(input: UpdateRemoteRotationProgressInput) => Promise<Result$1<RotationState, TransportError>>
Type(input: UpdateRemoteRotationProgressInput) => Promise<Result$1<RotationState, TransportError>>
completeRotation(input: CompleteRemoteRotationInput) => Promise<Result$1<CompletedRemoteRotation, TransportError>>
Type(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.

Last updated on August 4, 2026

Was this page helpful?