mtp/docs/WASM-CLIENT.md
Alex Emmet e939714557
Some checks failed
CI / checks (push) Failing after 2m54s
[Fix] Version Bump, Checks, tests CQ
2026-08-13 21:18:55 +02:00

758 lines
28 KiB
Markdown

# MTP WASM Client
The browser client is exposed through the `mtp` npm package. Most applications should use the SDK-first `MTPClient` API; direct generated WASM bindings remain available from `mtp/raw` for advanced integrations.
## Browser Compatibility
The SDK requires the browser to expose `WebTransport`. `MTPClient.isSupported()` is the runtime check. A browser without WebTransport cannot connect through this client.
| Requirement | Check |
| --- | --- |
| WebTransport API | `MTPClient.isSupported()` |
| Certificate trust | Browser validation or `serverCertificateHashes` |
| Secure context | Serve the application from HTTPS where required by the browser |
| Generated bindings | Run the Vite integration during development and build |
## Package Entry Points
```typescript
import { MTPClient } from "mtp";
import init, { WasmClient } from "mtp/raw";
import { mtp } from "mtp/vite";
```
- `mtp` exports the SDK-first `MTPClient` wrapper.
- `mtp/raw` exports the generated `wasm-bindgen` module and raw classes/functions.
- `mtp/vite` exports the Vite plugin that builds app-specific WASM bindings from your `type-maps.yaml`.
- `mtp/type-map` exports generated TypeScript unions for communication and data type names.
## Vite Type-Map Workflow
Browser apps provide their own type map. The Vite plugin runs `wasm-pack` during dev and build with `MTP_TYPE_MAPS` set, writes generated output under `node_modules/.vite/mtp/` by default, and aliases `mtp/raw` plus `mtp/type-map` to that generated output. Configuration: [Type Map](TYPE-MAP.md).
The browser build uses the map named by `protocol_version` and includes the
reserved MTP names. It does not advertise application names from other map
versions, because the generated WASM client is compiled for that one protocol
version. The selected version must exist in `type_maps`.
You do not need to publish, fork, or copy an app-specific generated WASM package.
The [web client example](../example/web-client/src/main.ts) shows the entry point. Its [Vite configuration](../example/web-client/vite.config.ts) shows the generated binding integration.
## SDK Quick Start
```typescript
import { MTPClient } from "mtp";
const client = await MTPClient.create({
url: "https://host.example.com:4433",
hostPublicKey,
credentials,
storage,
serverCertificateHashes: ["sha-256:abcd1234..."],
maxMessageSize: 1_000_000,
authTimeoutMs: 30_000,
pings: true,
logger: (event) => console.log(event),
});
const unsubscribe = client.subscribe("SomeType", (message) => {
console.log(message.type, message.data);
});
if (client.credentials?.clientId == null) {
await client.register();
} else {
await client.connect();
}
await client.send("SomeType", { value: "hello" });
const response = await client.request(
"SomeRequestType",
{ id: "abc" },
{ responseType: "SomeResponseType" },
);
client.raw.client; // underlying WasmClient instance
client.raw.bindings; // generated raw WASM module exports
unsubscribe();
client.disconnect();
```
`MTPClient.isSupported()` checks whether the current browser exposes WebTransport:
```typescript
if (!MTPClient.isSupported()) {
throw new Error("WebTransport is not available in this browser");
}
```
## MTPClient Options
| Option | Default | Purpose |
| --- | --- | --- |
| `url` | Required | WebTransport endpoint. |
| `descriptor` | None | Client label sent during connection setup. |
| `hostPublicKey` | None | Host public key bundle for authenticated login or registration. |
| `credentials` | None | Existing client ID and serialized keyring. |
| `credentialsStorageKey` | `mtp:credentials` | Key used by configured credential storage. |
| `storage` | None | Sync or async credential storage adapter. |
| `serverCertificateHashes` | Omitted | WebTransport certificate pins. |
| `maxMessageSize` | 16 MiB | Inbound and outbound frame limit. Values below frame overhead are rejected by the transport. |
| `authTimeoutMs` | No SDK timeout | Login and registration timeout. `undefined` leaves the promise pending until transport or peer failure. |
| `requestTimeoutMs` | 30 seconds | Default `request()` timeout. |
| `pings` | `false` | Protocol pings, or an object with `intervalMs`. |
| `logger` | No-op | Receives SDK state and error events. |
| `sessionStorage` | In-memory | E2EE session state storage. |
| `encryptedSecretProvider` | In-memory | Independent caller-managed encrypted secret storage. |
| `defaultSignatureVerificationPolicy` | `"ed25519"` | Receiver policy for protected signatures. |
`wasm` selects a custom generated WASM module. `MTPClient.create` validates positive safe-integer values for the numeric limits and timeout options.
## Differences from Native Client
The browser SDK uses WebTransport and JavaScript promises. The native client uses Rust futures, direct QUIC configuration, and `MTPConnection` handles. Browser pipes expose promise-based readers and writers; native pipes implement Tokio I/O traits.
### Native and Browser Credential Persistence
The `storage` option supplies the credential adapter. The adapter stores the client ID and serialized keyring after registration and returns them for later connections. The SDK does not select `localStorage` or IndexedDB for an application. Treat the serialized keyring as private key material.
`sessionStorage` and `encryptedSecretProvider` are separate caller-managed
stores. The latter exchanges `MTPEncryptedSecretRecord` values through
`set`, `get`, and `delete`; the `MTPClient` convenience methods are named
`setEncryptedSecret`, `getEncryptedSecret`, and `deleteEncryptedSecret`.
`MTPSessionManager` does not automatically route session state through the
provider. If session material must be encrypted at rest, the caller must make
that coordination explicit in its `MTPSessionStorage` implementation. Secret
IDs are opaque to MTP, so a caller can map its own state to the ID while
choosing the backing store and protecting its wrapping key.
### Direct Protected Messages
Use `sendProtected` when the destination is the frame receiver and no
intermediate relay needs a separately encrypted metadata layer. It keeps the
application communication type on the outer frame and encrypts an MTP-owned
signed envelope for the exact recipient bundles supplied by the caller. The
envelope authenticates `ProtectedVersion`, `MessageType`, `FinalRecipientId`,
`MessageId`, `CreatedAt`, and `Content`. The opening operation checks the
authenticated type and final recipient against the outer frame.
```typescript
await client.sendProtected("ProtectedMessage", { Content: "hello" }, {
receiverId: recipientId,
recipients: [recipientPublicKey],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
exposeSender: false,
});
```
The protection purposes are application-defined domain-separation values.
`exposeSender` controls only the outer frame sender; the protected value remains
signed in either case. If `identity` is omitted, the SDK uses stored registered
credentials and rejects the operation when no usable protection identity is
available.
An unauthenticated connection can still send a protected value when the caller
provides an explicit `identity` with the signer ID and keyring. The connection's
authentication state and the protected signer's identity are independent.
When `signatureSuite` is omitted, protected send helpers use Ed25519 even when
the signing keyring also contains post-quantum keys. This matches the default
receiver policy. Use `signatureSuite: "dual"` together with
`signaturePolicy: "dual"` when both sides explicitly require hybrid
signatures.
Open a direct protected frame with the recipient keyring and a resolver that
receives the claimed, unverified signer ID only as a trusted-key lookup key:
```typescript
const message = await client.openProtected(frame, {
recipient: {
id: recipientId,
keyring: recipientKeyring,
keyringHistory: previousRecipientKeyrings,
},
expectedReceiverId: recipientId,
expectedSignerId: signerId,
resolveSignerPublicKeys: (id) => signerDirectory.get(id) ?? [],
signaturePolicy: "dual",
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
replayGuard,
});
console.log(message.type, message.signerId, message.messageId, message.data);
```
`protectedVersion`, `finalRecipientId`, `signerId`, `messageId`, and `createdAt`
are taken from the verified protected envelope. `outerSender`, when present,
must equal the authenticated signer.
Protected application data may be any supported MTP `DataValue`, including
scalar, byte, array, and container values. Direct opening uses a bounded
process-local duplicate-suppression guard by default. The bounded cache can
evict old entries, so supply a durable `replayGuard` keyed by authenticated
signer and message ID when replay protection must survive eviction, reloads, or
multiple receiver processes. The guard also receives authenticated
`createdAt` metadata, which is not part of the replay key.
`subscribeProtected` uses the same opening and verification path:
```typescript
const unsubscribe = client.subscribeProtected(
"ProtectedMessage",
(message, frame) => handleMessage(message.data, frame),
{
recipient: { id: recipientId, keyring: recipientKeyring },
resolveSignerPublicKeys: (id) => signerDirectory.get(id) ?? [],
signaturePolicy: "dual",
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
},
);
```
Each `subscribeProtected` registration owns its own bounded default replay
guard, so multiple handlers receive the same raw frame through the WASM
fan-out dispatcher. Pass the same caller-owned `replayGuard` deliberately when
several subscriptions should share replay state.
### Sealed Relay Messages
`sendSealedRelay` uses the reserved opaque `Relay` communication type. Its
inner message type must be an application communication type, not an MTP
control type. The outer frame contains no sender and exposes only the next-hop
receiver. The
signed relay metadata contains the generic `signerId`, `finalRecipientId`,
`messageId`, `createdAt`, application `metadata`, and an opaque encrypted
content value. `createdAt` is generated as Unix epoch milliseconds. For
example, `2026-08-11T12:00:00.000Z` is `1786449600000`.
```typescript
const data = { Content: "hello" };
await client.sendSealedRelay("ProtectedMessage", data, {
finalRecipientId,
nextHopId,
metadataRecipients: [
relayPublicKey,
recipientPublicKey,
],
contentRecipients: [
recipientPublicKey,
],
metadata: {
ExampleMetadata: "routing context",
},
});
client.subscribeSealedRelay(
"ProtectedMessage",
(message, frame) => handleMessage(message.data, frame),
{
recipient: {
id: finalRecipientId,
keyring: recipientKeyring,
},
expectedSignerId: signerId,
resolveSignerPublicKeys: () => [senderPublicKey],
},
);
```
The caller supplies the exact metadata and content recipient sets; the SDK
does not infer application topology. Set `signaturePolicy: "dual"` to require
hybrid signatures explicitly, and install a durable `replayGuard` so a valid
`(signerId, messageId)` is dispatched only once.
Each sealed-relay or metadata subscription likewise gets an independent
bounded default guard. This preserves fan-out when multiple handlers inspect
the same outer `Relay` frame; an explicitly supplied guard is shared by the
subscriptions that receive it.
Applications choose between direct protected delivery and sealed relay based
on topology and metadata-access requirements. Prefer `sendProtected` for a
direct destination. Use `sendSealedRelay` when a next hop must route or store a
message and the application needs metadata recipients to differ from content
recipients. Neither construction requires connection authentication, although
the host can associate an authenticated connection with its registered MTP
identity.
For metadata-only access, call `openRelayMetadata` or subscribe with
`subscribeRelayMetadata`. These operations authenticate the metadata and
expose `encryptedContent` for forwarding without attempting content
decryption. A final recipient calls `openRelayContent` after metadata
verification; the returned `MTPVerifiedRelayContent` includes the application
type and data plus `signerId`, `finalRecipientId`, `messageId`, `createdAt`,
and generic metadata fields. These are authenticated protected identities, not
the clear outer sender and next-hop receiver.
Relay content inherits the authenticated metadata's `signaturePolicy` when no
content override is supplied. A different content policy is rejected so the
two relay layers cannot be verified under conflicting rules.
Metadata passed to a `subscribeRelayMetadata` handler is callback-scoped and is
disposed after the handler resolves. Do not retain it for a later
`openRelayContent` call; use `openRelayMetadata` directly when a longer-lived
verified capability is needed, and call `dispose()` when finished.
When signer key history is used, `signerPublicKeys` exposes the trusted
candidates, `matchedSignerKeyIndex` identifies the key that verified the
metadata, and `matchedSignerPublicKey` returns that exact bundle.
Protected receive operations accept an optional `recipient` decryption
identity. Its `keyring` controls decryption and its optional `id` is used only
for final-recipient validation. The identity is independent from connection
authentication. Metadata opening does not require the identity ID to match the
clear next-hop receiver, so a forwarded frame can be opened by a metadata
recipient or final recipient with the appropriate keyring. When `recipient` is
omitted, stored registered credentials remain the convenience fallback.
To open values encrypted for a rotated recipient, provide `keyringHistory` on
the decryption identity. The current `keyring` is tried first, followed by
history entries from newest to oldest. Exact duplicate byte sequences are
removed without changing the caller's input arrays. An empty current keyring
or an empty history entry is rejected.
Generic MTP `DataValue` inputs accept `bigint` for exact integer values. An
integral JavaScript `number` outside the safe-integer range is rejected, so it
cannot silently become an imprecise float. Use `bigint` for large signed or
unsigned integers.
For streams, prefer `createEncryptedPipe` and `acceptEncryptedPipe`; they bind
the actual pipe ID and local identity automatically. The lower-level
`initiateMTPPipeSession` API also accepts multiple recipient bundles for a
group bootstrap. Group membership changes require a fresh session ID and
recipient set. Live calls that need forward secrecy can use the exported
duplex `initiateMTPForwardSecurePipeSession` and
`acceptMTPForwardSecurePipeSession` helpers.
The convenience pipe methods intentionally require registered client
credentials because they use the connection's registered identity as the
endpoint identity. Use the lower-level session functions when transport
authentication and cryptographic endpoint identity must remain independent.
Receive-side signature policy is independent from the recipient keyring. Use
`signaturePolicy` on protected receive and encrypted-pipe accept operations,
or configure `defaultSignatureVerificationPolicy` on the client. The sender's
`signatureSuite` selects how local values are signed and is a separate choice.
Both sender and receiver default to Ed25519; `dual` is always an explicit
choice on each side.
### Native and Browser Certificate Checks
WebTransport certificate pins must match the server certificate hash. A pin mismatch is a TLS failure, not an MTP authentication failure. Check the browser network panel, endpoint origin, and WebTransport CONNECT path before inspecting frames.
## Credentials And Storage
Authenticated connections need stable key material. Pass `credentials` when you already have a client ID and serialized keyring, or pass a small `storage` object and let the SDK persist credentials after registration.
```typescript
const storage = {
getItem: (key: string) => localStorage.getItem(key),
setItem: (key: string, value: string) => localStorage.setItem(key, value),
removeItem: (key: string) => localStorage.removeItem(key),
};
const client = await MTPClient.create({
url: "https://host.example.com:4433",
hostPublicKey,
storage,
});
const clientId = client.credentials?.clientId == null
? await client.register()
: (await client.connect(), client.credentials.clientId);
```
The storage contract is intentionally small and may be sync or async:
```typescript
interface MTPCredentialStorage {
getItem(key: string): string | null | Promise<string | null>;
setItem(key: string, value: string): void | Promise<void>;
removeItem(key: string): void | Promise<void>;
}
```
`credentials` can also be supplied directly:
```typescript
const client = await MTPClient.create({
url: "https://host.example.com:4433",
hostPublicKey,
credentials: {
clientId: 42n,
keyring: savedKeyringBytes,
},
});
await client.connect();
```
`client.credentials` returns the current public credential object. Call `clearCredentials()` to remove in-memory credentials and delete the configured storage key.
## Connection Methods
- `connect()` opens a connection. If credentials include a `clientId` and `hostPublicKey` is available, it uses authenticated login; otherwise it uses unauthenticated connect.
- `register()` performs authenticated registration and persists the assigned client ID when storage is configured.
- `disconnect()` stops protocol pings and closes the underlying WebTransport session.
For certificate pinning, pass WebTransport certificate hashes:
```typescript
await MTPClient.create({
url: "https://host.example.com:4433",
serverCertificateHashes: ["sha-256:abcd1234..."],
});
```
If hashes are omitted, the browser uses its normal TLS root store.
`maxMessageSize` caps inbound and outbound MTP frames before buffering/sending.
`authTimeoutMs` bounds connect/login/register promises at the SDK layer.
`requestTimeoutMs` sets the default timeout for `request()` calls; a request can override it with `timeoutMs` in its options.
## Streams
The browser client uses one WebTransport session per `MTPClient` instance.
`send()`, `request()`, and `subscribe()` all operate over that session; the SDK does not expose browser stream objects directly.
Use the normal message APIs to send and receive over that session:
```typescript
const client = await MTPClient.create({ url, hostPublicKey });
await client.connect();
const unsubscribe = client.subscribe("SomeType", (message) => {
console.log(message.data);
});
await client.send("SomeType", { value: "hello" });
unsubscribe();
```
Internally, each outbound MTP frame is written to a new WebTransport unidirectional stream as a four-byte big-endian length followed by the frame, then that stream is closed. Incoming frames are read from the session's incoming unidirectional streams. The reader accepts both one-frame streams and native peers that place several frames on a persistent stream, so browser and native clients interoperate without stream configuration.
The SDK owns stream lifetime and framing. Do not create browser streams for MTP frames yourself through the SDK. For direct generated bindings, use `client.raw.client` or import `WasmClient` from `mtp/raw`; a `WasmClient` still owns one active WebTransport session, so create another instance for an independent connection.
## Sending, Requests, Subscriptions, And Pings
`send` accepts either a typed message or a prebuilt raw frame:
```typescript
await client.send("SomeType", { value: "hello" });
await client.send(rawFrameBytes);
```
Typed sends are encoded by the generated WASM binding using the app type map. Optional frame metadata can be passed as the third argument:
```typescript
await client.send("SomeType", { value: "hello" }, {
id: 7,
sender: client.credentials?.clientId ?? 0n,
});
```
`request` sends one frame and resolves with the parsed response carrying the same frame id. If the matching response has a different `responseType`, the promise rejects with a response-type error. A timeout rejects the promise and removes the pending request:
```typescript
const response = await client.request(
"SomeRequestType",
{ id: "abc" },
{ responseType: "SomeResponseType" },
);
```
`subscribe` registers a message-type handler and returns an unsubscribe function:
```typescript
const unsubscribe = client.subscribe("SomeType", (message) => {
console.log(message.id, message.sender, message.data);
});
unsubscribe();
```
Protocol ping behavior is defined in [Protocol Reference](PROTOCOL-REFERENCE.md#protocol-keepalive). The SDK configuration is:
```typescript
await MTPClient.create({
url: "https://host.example.com:4433",
pings: { intervalMs: 30_000 },
});
```
Use `pings: true` for the default interval.
## Pipes
Pipes are byte-oriented streams over WebTransport. The `PipeRequest` type and
description are clear transport metadata; raw stream bytes are not protected
by MTP. For sensitive calls, files, or application streams, wrap the accepted
pipe with `MTPEncryptedPipeWriter` or `MTPEncryptedPipeReader`.
### Outgoing Pipes
`createPipe` sends a `PipeRequest` frame and returns a handle. Call `wait()` to block until the remote peer accepts or denies:
```typescript
const handle = await client.createPipe("file-transfer");
const writer = await handle.wait();
if (writer == null) {
console.log("host denied the pipe");
return;
}
await writer.write(new Uint8Array([0x01, 0x02, 0x03]));
await writer.write(chunk);
await writer.close();
```
`writer.close()` sends a QUIC stream FIN. `writer.abort()` resets the stream abruptly. Each `write` resolves when the chunk has been handed to the transport; it does not wait for the peer to consume it.
### Encrypted Pipe Records
`initiateMTPPipeSession` and `acceptMTPPipeSession` perform the signed/KEM
protected pipe-session offer and return the encrypted record wrapper. The
offer binds the session ID, pipe ID, endpoint IDs, direction, and purpose. Do
not derive the initial chain key from the clear description or pipe ID alone.
```typescript
import {
initiateMTPPipeSession,
} from "mtp";
const encryptedWriter = await initiateMTPPipeSession(
writer,
{
sessionId: new TextEncoder().encode(`file-transfer/${writer.pipeId}`),
pipeId: writer.pipeId,
senderId: ownClientId,
recipientId: hostClientId,
purpose: 0x40,
direction: 0,
},
ownKeyring,
hostPublicKeyBundle,
);
await encryptedWriter.writeRecord(chunk);
await encryptedWriter.close();
```
`writeRecord` and `readRecord` use XChaCha20-Poly1305 with ordered sequence
numbers bound to the session context. Each record advances an HKDF chain and
uses a one-use message key. Record insertion, removal, reordering, or
modification fails authentication. The wrapper is intentionally separate from
the raw `PipeWriter`/`PipeReader` transport primitives.
The handle and writer expose `pipeId` and `description`:
```typescript
console.log(handle.pipeId, handle.description);
console.log(writer.pipeId);
```
### Incoming Pipes
Set a handler to receive pipe requests from the remote peer:
```typescript
client.setOnPipeRequest((request) => {
console.log("incoming pipe", request.pipeId, request.description);
// accept or deny asynchronously
});
```
Accept a request to receive a `PipeReader`:
```typescript
client.setOnPipeRequest(async (request) => {
if (request.description === "file-transfer") {
const reader = await client.acceptPipe(request.pipeId);
while (true) {
const chunk = await reader.read();
if (chunk == null) break; // stream closed by peer
processChunk(chunk);
}
} else {
await client.denyPipe(request.pipeId);
}
});
```
`reader.read()` resolves with a `Uint8Array` or `null` when the peer closes the stream. The reader exposes `pipeId` and `description`:
```typescript
console.log(reader.pipeId, reader.description);
```
### Pipe Handshake
1. The initiator calls `createPipe(description)`; the SDK sends a `PipeRequest` frame with a random `pipeId` and the description.
2. The receiver's `setOnPipeRequest` callback fires with `{ pipeId, description }`.
3. The receiver calls `acceptPipe(pipeId)`; the SDK sends a `PipeResponse` with `Accepted = true` and opens a new unidirectional stream for byte transport.
4. The initiator's `handle.wait()` resolves with a `PipeWriter` bound to that stream. Sensitive applications then perform their signed/encrypted session-key setup and construct an encrypted record wrapper.
5. If the receiver calls `denyPipe(pipeId)`, `handle.wait()` resolves with `null`.
Pipes share the same WebTransport session as message frames; they do not need a separate connection.
## Logger Events
The SDK logger receives parsed events:
```typescript
type MTPLogEvent =
| { hint: "info" | "warning"; type: string; data: unknown }
| { hint: "error"; type: string | "error"; error: string };
```
Incoming non-error frames and sent frames are logged as `info`. Error frames and transport errors are logged as `error`.
## Advanced Raw Bindings
Use `mtp/raw` when you need direct access to the generated `wasm-bindgen` API:
```typescript
import init, {
ConnectionConfig,
WasmClient,
ed25519_generate,
keyring_from_ed25519,
} from "mtp/raw";
await init();
const rawClient = new WasmClient(
(state) => console.log("state", state),
(frame) => console.log("message", frame),
(error) => console.error(error),
);
const config = new ConnectionConfig("https://host.example.com:4433");
config.client_id = 42n;
await rawClient.connect(config);
config.free();
```
Raw callbacks receive parsed frames, not application-specific SDK objects:
```typescript
interface ParsedEncryptedValue {
kind: "encrypted";
encryptionType: number;
purpose: number;
recipientCount: number;
encoded: Uint8Array;
}
interface ParsedSignedValue {
kind: "signed";
signatureType: number;
purpose: number;
signerId: bigint;
value: ParsedDataValue;
}
type ParsedDataValue =
| boolean
| number
| bigint
| string
| Uint8Array
| ParsedDataValue[]
| { [key: string]: ParsedDataValue }
| ParsedEncryptedValue
| ParsedSignedValue
| null;
interface ParsedFrame {
id?: number;
type: string;
sender?: bigint;
receiver?: bigint;
data: ParsedDataValue;
raw: Uint8Array;
}
```
### Frames
Raw message helpers that remain available include:
- `build_frame(messageType, data, options?)`
- `build_ping_frame(clientId, description, timestamp, data)`
- `parse_frame(frame)`
- `format_frame(frame)`
- `parse_auth_response(frame)`
The SDK export also exposes the same frame codec through `codec`:
```typescript
import { codec } from "mtp";
const frame = codec.encode("SomeType", { value: "hello" });
const parsed = codec.decode(frame);
const display = codec.format(frame);
```
### Crypto
Raw crypto and key helpers include:
- `ed25519_generate()`
- `ed25519_verify(publicKey, message, signature)`
- `keyring_generate()`
- `keyring_from_ed25519(secretKey, publicKey)`
- `WasmKeyring.from_bytes(bytes)` and `keyring.to_bytes()`
- `keyring.validate_encryption()` for envelope decryption roles
- `keyring.validate_full()` for complete hybrid identities
- `WasmPublicKeyBundle.from_bytes(bytes)` and `bundle.to_bytes()`
- `WasmEd25519Signer`
- `WasmChaCha20Poly1305`
- `sign_data_value_with_keyring` and `verify_data_value_with_policy` (both require an explicit signature suite), plus `encrypt_data_value`, `encrypt_data_value_for_recipients`, and `decrypt_data_value`
- `parse_data_value` and `encode_data_value`
- `wasm_sha256`, `wasm_sha256_double`, `wasm_hkdf_expand`, and `wasm_derive_encryption_key`
Raw authenticated login and registration map directly to the Rust WASM layer:
```typescript
const generated = ed25519_generate();
const keyringBytes = keyring_from_ed25519(generated.secretKey, generated.publicKey);
const registeredId = await rawClient.auth_register(
config,
hostPublicKeyBytes,
keyringBytes,
);
const confirmedId = await rawClient.auth_connect(
config,
hostPublicKeyBytes,
keyringBytes,
registeredId,
);
```
### Pipes
The raw `WasmClient` exposes the same pipe operations as the SDK wrapper. The shared lifecycle is in [Pipes](PIPES.md); raw bindings use snake_case names.
```typescript
rawClient.set_on_pipe_request((event) => {
void rawClient.accept_pipe(event.pipeId);
});
const handle = await rawClient.create_pipe("file-transfer");
const writer = await handle.wait();
if (writer) {
await writer.write(chunk);
await writer.close();
}
```
A `WasmClient` manages one active WebTransport session. Create a new instance for independent connections, and call `free()` or `[Symbol.dispose]()` on raw WASM objects when you want to release memory eagerly.
### State Management
A `WasmClient` owns one active WebTransport session. Create a separate client for each independent connection. Call `free()` or `[Symbol.dispose]()` on raw WASM objects when the application no longer needs them. SDK session and encrypted secret persistence are documented in [Security](SECURITY.md#browser-end-to-end-encryption).