Transport API reference
Complete reference for Fetch and tRPC server handlers, browser transports, bounded JSON parsing, protocol responses, options, errors, and custom transport boundaries.
Transports move vault lifecycle metadata between VaultClient and the authenticated VaultServer.
They never carry application record plaintext, Recovery Kit contents, secrets, or raw keys.
Fetch transport
pnpm add @ownfold/fetch@beta
@ownfold/fetch works in runtimes with standard Request, Response, Headers, and fetch.
Server handler
import { createVaultFetchHandler } from "@ownfold/fetch"
export const handleOwnfold = createVaultFetchHandler({
server: vaultServer,
basePath: "/api/ownfold",
isOriginAllowed: ({ request }) =>
new URL(request.url).origin === process.env.PUBLIC_APP_ORIGIN,
})
serverVaultServer
VaultServerbasePath?string
stringisOriginAllowed?(input: { readonly request: Request }) => boolean | Promise<boolean>
(input: { readonly request: Request }) => boolean | Promise<boolean>| Option | Default | Contract |
|---|---|---|
server |
Required | The configured authenticated VaultServer. |
basePath |
/api/ownfold |
Exact mount path used for operation routing. Keep client and server equal. |
isOriginAllowed |
Same-origin checks remain framework-owned | Return true only for origins the host trusts to send credentialed mutations. |
createVaultFetchHandler returns (request: Request) => Promise<Response>. It handles GET and
POST lifecycle operations below basePath, rejects unknown paths and methods, parses strict input,
and maps typed errors to remote-safe JSON.
Browser client transport
import { createFetchVaultTransport } from "@ownfold/fetch"
const transport = createFetchVaultTransport({
baseURL: "/api/ownfold",
credentials: "same-origin",
headers: async () => ({ "x-csrf-token": await csrfToken() }),
})
baseURL?string
stringcredentials?RequestCredentials
RequestCredentialsfetch?typeof globalThis.fetch
typeof globalThis.fetchheaders?HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
HeadersInit | (() => HeadersInit | Promise<HeadersInit>)| Option | Default | Use |
|---|---|---|
baseURL |
/api/ownfold |
Relative or absolute handler URL. |
credentials |
same-origin |
Browser credential policy. Use include only with a deliberate cross-origin session design. |
fetch |
globalThis.fetch |
Runtime implementation or a typed test double. |
headers |
None | Static headers or an async provider evaluated for each request. |
The returned object implements both VaultTransport and VaultRotationTransport. It validates the
response envelope before returning a result. A successful HTTP status with malformed JSON is still
an error.
Protocol constants
| Export | Value | Meaning |
|---|---|---|
VAULT_HTTP_DEFAULT_BASE_PATH |
/api/ownfold |
Default handler and transport mount. |
VAULT_HTTP_MAX_BODY_BYTES |
65536 |
Default maximum JSON body size in bytes. |
Bounded JSON utilities
readBoundedJsonRequest(request, options?) and readBoundedJsonResponse(response, options?) return
Result<unknown, InvalidInputError>. They check declared and streamed size, decode UTF-8, and parse
JSON without casting it to an application type.
maxBodyBytes?number
numberUse them when building an adjacent custom endpoint with the same body-size behavior. After reading,
parse the returned unknown through an Ownfold namespace or Standard Schema.
HTTP response envelope
oktrue
trueSuccess responses contain { ok: true, data }. Failure responses contain only the stable code and
remote-safe message. Do not attach stack traces, driver causes, session values, or request bodies.
Route surface
The handler exposes lifecycle coordination operations corresponding to the server methods:
| Family | Operations |
|---|---|
| Vault | get, create, mark recovery verified, replace Recovery Kit |
| Devices | list, register, touch, revoke |
| Pairing | create, read, approve, cancel |
| Rotation | read, begin, update progress, complete |
Application encrypted-record CRUD is deliberately absent. Build application routes that authorize
the row and call validateEncryptedRecordWrite before persistence.
Origin and CSRF policy
The Fetch handler accepts an async isOriginAllowed hook because trusted origins are application
configuration. Framework bridges with native Node requests require an explicit origin option. A
valid session cookie is not, by itself, a cross-site request defense.
For state-changing routes:
- allow only exact configured origins;
- reject missing or invalid origin evidence according to the host’s non-browser policy;
- keep cookies
Secure,HttpOnly, and appropriatelySameSite; and - add the host’s CSRF token strategy when credential policy requires it.
Error mapping
| Failure | Transport result |
|---|---|
| Network failure or aborted request | TransportError with VAULT_NETWORK_FAILED |
| Malformed or oversized response | TransportError/input error with a stable invalid-response code |
| Typed server rejection | The corresponding Ownfold error reconstructed from its stable code |
| Unknown server failure | Generic transport/server error; no raw body is trusted |
Retry reads and operations with stable idempotency identities. Do not automatically replay a mutation merely because the connection ended before its response arrived; reload remote state first.
tRPC
pnpm add @ownfold/trpc@beta @trpc/server
The root entry point builds the server router. The /client entry point adapts a typed tRPC client
to the same browser transport contracts.
createVaultTRPCRouter(options)
import { createVaultTRPCRouter } from "@ownfold/trpc"
export const ownfoldRouter = createVaultTRPCRouter({
server: vaultServer,
})
serverVaultServer
VaultServerThe context must contain a standard authenticated Request. The router does not accept user ID in
procedure input.
requestRequest
RequestProcedures
| Query | Output |
|---|---|
getVault |
`VaultMetadata |
listDevices |
readonly DeviceSummary[] |
getPairing |
`PairingRequest |
getRotation |
`RotationState |
| Mutation | Input family |
|---|---|
createVault, markRecoveryVerified, replaceRecoveryKit |
Strict remote vault inputs |
registerDevice, touchDevice, revokeDevice |
Strict remote device inputs |
createPairing, approvePairing, cancelPairing |
Strict pairing inputs |
beginRotation, updateRotationProgress, completeRotation |
Strict remote rotation inputs |
The router uses the same server pipeline and error codes as the Fetch handler. tRPC errors are an integration boundary, not a different vault behavior.
trpcVaultTransport(client)
import { trpcVaultTransport } from "@ownfold/trpc/client"
const transport = trpcVaultTransport(trpcClient.ownfold)
getVault{
query(): Promise<RemoteVaultState | null>
}
{
query(): Promise<RemoteVaultState | null>
}createVault{
mutate(input: CreateRemoteVaultInput): Promise<RemoteVaultState>
}
{
mutate(input: CreateRemoteVaultInput): Promise<RemoteVaultState>
}markRecoveryVerified{
mutate(input: MarkRecoveryVerifiedInput): Promise<RemoteVaultState>
}
{
mutate(input: MarkRecoveryVerifiedInput): Promise<RemoteVaultState>
}replaceRecoveryKit{
mutate(input: ReplaceRemoteRecoveryKitInput): Promise<RemoteVaultState>
}
{
mutate(input: ReplaceRemoteRecoveryKitInput): Promise<RemoteVaultState>
}listDevices{
query(): Promise<readonly DeviceSummary[]>
}
{
query(): Promise<readonly DeviceSummary[]>
}registerDevice{
mutate(input: RegisterRemoteDeviceInput): Promise<DeviceSummary>
}
{
mutate(input: RegisterRemoteDeviceInput): Promise<DeviceSummary>
}touchDevice{
mutate(input: TouchRemoteDeviceInput): Promise<DeviceSummary>
}
{
mutate(input: TouchRemoteDeviceInput): Promise<DeviceSummary>
}revokeDevice{
mutate(input: RevokeRemoteDeviceInput): Promise<DeviceSummary>
}
{
mutate(input: RevokeRemoteDeviceInput): Promise<DeviceSummary>
}createPairing{
mutate(input: CreatePairingRequestInput): Promise<PairingRequest>
}
{
mutate(input: CreatePairingRequestInput): Promise<PairingRequest>
}getPairing{
query(input: GetPairingRequestInput): Promise<PairingRequest | null>
}
{
query(input: GetPairingRequestInput): Promise<PairingRequest | null>
}approvePairing{
mutate(input: ApprovePairingRequestInput): Promise<PairingRequest>
}
{
mutate(input: ApprovePairingRequestInput): Promise<PairingRequest>
}cancelPairing{
mutate(input: CancelPairingRequestInput): Promise<PairingRequest>
}
{
mutate(input: CancelPairingRequestInput): Promise<PairingRequest>
}getRotation{
query(): Promise<RotationState | null>
}
{
query(): Promise<RotationState | null>
}beginRotation{
mutate(input: BeginRemoteRotationInput): Promise<RotationState>
}
{
mutate(input: BeginRemoteRotationInput): Promise<RotationState>
}updateRotationProgress{
mutate(input: UpdateRemoteRotationProgressInput): Promise<RotationState>
}
{
mutate(input: UpdateRemoteRotationProgressInput): Promise<RotationState>
}completeRotation{
mutate(input: CompleteRemoteRotationInput): Promise<CompletedRemoteRotation>
}
{
mutate(input: CompleteRemoteRotationInput): Promise<CompletedRemoteRotation>
}The adapter requires only the query/mutation methods in VaultTRPCClient; it does not depend on a
specific React Query binding. Procedure failures become TransportError with stable safe details.
Custom transport
Implement VaultTransport & VaultRotationTransport when neither Fetch nor tRPC matches the host.
Use the exact inputs and outputs in Adapter and transport contracts, validate
all untrusted responses, preserve error codes, and never add userId to client-callable inputs.
See the complete export index for every transport export.