npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mte-relay-browser

v5.0.0

Published

A browser-focused TypeScript SDK that wraps native `fetch` with transparent MTE Relay transport.

Readme

mte-relay-browser (TypeScript / Browser)

A browser-focused TypeScript SDK that wraps native fetch with transparent MTE Relay transport.

The SDK preserves the caller experience (fetch-compatible request/response), while encoding outbound requests into MTE binary frames and decoding inbound relay frames back into normal HTTP responses.

Release identity

This SDK is 5.0.0, the first stable release for Relay v5, which speaks frame version 2. Every MTE Relay v5 web package carries the relay's major version: mte-relay-browser, mte-relay-browser-public-client, and mte-relay-angular. The temporary v6 branch name did not represent a separate product release. Published versions through 5.0.0-beta.11 use the earlier protocol, which a v5 relay refuses with 478. Publish this release with npm run publish:latest.

Features

Uploads above 32 MiB are staged as blobs. The client reads and encrypts at most 8 MiB of plaintext per batch, then sends the encoded blob in one HTTP request. This avoids multi-gigabyte contiguous JavaScript buffers and works over HTTP/1.1. The full encoded upload is still staged before sending; browser blob storage, memory, disk space and request timeouts still apply. Smaller bodies retain the byte-buffer path. Automatic retries can replay either representation.

  • fetch-compatible wrapper API
  • MTE/MKE frame encode + decode lifecycle
  • Lazy per-origin relay sessions (auto auth + pair on first request to each server)
  • Multi-pair round-robin state pool with warm baseline (minPairs, default 5), startup size (initialPairs, default 8), and hard ceiling (maxPairs, default 15)
  • Early background refill when idle pairs drop to or below minPairs
  • Immediate MteRelayCapacityError when pool is at maxPairs with no idle pair
  • Every caller header is encrypted by default; unencryptedHeaders sends a chosen subset as ordinary HTTP headers on the relay POST
  • Relay discovery: the SDK reads the relay object from the auth response to fail early on protocol, encode type, or Kyber mismatches, tune keepalive, reject oversized bodies before upload, and warn when a plain header will not reach the origin
  • Pass through routes advertised by the relay are sent as plain fetch calls, no frame
  • Sticky routing: the SDK echoes the relay's X-MTE-Relay-Route token so a header hashing load balancer can pin a client to one relay pod
  • Per-pair serialized execution to protect stateful encoder/decoder usage
  • Native text/event-stream responses stay streaming instead of being buffered
  • MKE-framed responses, including decoded text/event-stream bodies, are parsed metadata-first and streamed through a browser Response with one progressive StartDecrypt / DecryptChunk / FinishDecrypt session across the body
  • Explicit one-time initialization via initMteRelayClient(...)
  • Relay error handling for HTTP 559–569
  • Full re-pair flow for relay HTTP 559–569 and local MTE encode/decode failures (drop state, re-authenticate, re-pair, then surface the repaired error)
  • Periodic keep-alive pings to /api/mte-keepalive so idle clients (backgrounded tabs, long-lived single-page apps) do not lose their pair pool to server-side session timeout

Prerequisites

  • Node.js 18+
  • npm
  • Running MTE Relay Server, built against the MTE server library (see below)
  • Valid MTE company name and license key
  • Browser runtime with fetch, AbortController, and WebCrypto support
    • https://caniuse.com/?search=fetch
    • https://caniuse.com/?search=AbortController
    • https://caniuse.com/?search=Crypto

MTE library build pairing

The MTE library ships in three builds and they are not interchangeable:

A client build pairs only with a server build. An ANY build pairs only with another ANY build. There is no build that pairs with everything.

This SDK links the client build. The mte peer dependency is the client WASM package, so there is nothing to choose here. What it means is a requirement on the other end:

| Process | Build | | --- | --- | | This SDK, in the browser | client, already fixed by the npm package | | The relay it talks to | server. In mte-relay, copy mte/mrs-server-lib/ into mte/lib/ |

A relay running the ANY build cannot decode a single request from this SDK. The failure is immediate and total, not intermittent: mte_status_token_does_not_exist on the first message of a pair, then mte_status_seq_mismatch on everything after it. If pairing succeeds and then every request fails to decode, check the relay's library build before anything else.

Note that the relay repository defaults to the ANY build, because its own test suite runs a client and a server in one process and only an ANY build pairs with itself. Running a relay for this SDK means swapping the library and clearing the cgo cache first. The full rule, the per process table, and a cross build interop check are documented in mte-relay/mte/README.md.

scripts/interop-fixture.mjs in this repository is the browser half of that check. It passes real ciphertext to and from a relay build without going through the relay protocol, so it answers "do these two libraries pair" on its own.

Install

npm install

Build

npm run build

npm run build creates the distributable package without JavaScript source maps and without TypeScript declaration maps.

npm run build:with-map

npm run build:with-map builds the package with Vite source maps and TypeScript declaration maps.

Scripts

  • npm run dev starts the Vite development server.
  • npm run build creates the production package without JavaScript source maps and without TypeScript declaration maps.
  • npm run build:with-map creates the production package with JavaScript source maps and TypeScript declaration maps.
  • npm run publish:beta:dry-run rebuilds the package and previews a beta publish without uploading anything.
  • npm run publish:beta asks for confirmation, rebuilds the package, and publishes the current version with the beta dist-tag.
  • npm run publish:latest asks for confirmation, rebuilds the package, and publishes the current version with the default npm dist-tag.
  • npm run test runs Vitest once.
  • npm run test:watch starts Vitest in watch mode.
  • npm run e2e:local [relayOrigin] runs scripts/e2e-local-relay.mjs against a locally running v5 relay. See below.

Local end to end check

npm run e2e:local exercises the built dist/ (run npm run build first) under Node against a real relay: JSON GET, POST with body and encrypted headers, 204, query string, SSE over MKE, and the metadata size bounds under both encode types. It needs a v5 relay running locally whose DOMAIN_MAP routes "*" to https://jsonplaceholder.eclypses.com, built with the MRS server library (copy mte/mrs-server-lib into mte/lib in the server repo and build with go build -a). The script takes the relay origin as its one argument; the default is http://127.0.0.1:8090. When the relay runs inside WSL and the script runs on Windows, pass http://localhost:8090. Exit code is 0 only when every check passes.

Publishing Beta Releases

Use this workflow when publishing a prerelease to npm:

  1. Check the version in package.json and confirm it is the beta version you want to publish.
  2. If you need the next beta number, run:
npm version prerelease --preid beta
  1. Rebuild and preview the published package:
npm run publish:beta:dry-run
  1. Publish the beta package:
npm run publish:beta

Notes:

  • npm publish -beta is not a valid npm command. Use npm publish --tag beta instead.
  • npm run publish:beta requires interactive confirmation. Type publish when prompted to continue.
  • npm run build is the publish-safe build. It disables JavaScript source maps and TypeScript declaration maps.
  • prepack runs npm run build before npm pack and npm publish, so the tarball is rebuilt from a clean, map-free dist.
  • package.json publishes only the dist directory via the files array, so raw source files from src/ are not included in the npm package.

Local Development (npm link)

Use this when developing this package and consuming it from another local project.

  1. Build and register global link from this repo:
npm run build
npm link
  1. In another local project, link this package:
npm link mte-relay-browser
  1. Use it normally in code:
import { initMteRelayClient, mteFetch } from "mte-relay-browser";
  1. After changing this package, rebuild here and restart the consumer app if needed:
npm run build
  1. Unlink when done:
# in consumer project
npm unlink mte-relay-browser

# in this package repo (remove global link)
npm unlink -g mte-relay-browser

SDK Location

Primary implementation lives in:

  • src/mte-relay/

Key modules:

  • client.ts — lifecycle + fetch wrapper
  • pairing.ts — Kyber key exchange + cross-wired pair instantiation
  • protocol/frameWriter.ts — request frame construction
  • protocol/frameReader.ts — response frame parsing
  • transport.ts — relay POST transport (using content-type=application/octet-stream)
  • headerPolicy.ts — unencryptedHeaders validation and the encrypted versus plain header split
  • discovery.ts — parsing and applying the relay's discovery object
  • errors.ts — relay error classification and error type
  • index.ts — public exports

Configuration

initMteRelayClient(options) accepts:

type InitMteRelayOptions = {
  companyName: string;
  licenseKey: string;
  minPairs?: number; // warm baseline pair count, default 5
  initialPairs?: number; // pairs created at session start, default 8
  maxPairs?: number; // hard pool ceiling, default 15
  defaultEncodeType?: "MTE" | "MKE"; // default "MKE"
  sequenceWindow?: number; // default -63, or the relay's value when discovery reports one
  timeWindow?: number; // default 1000
  httpTimeoutMs?: number; // default 30000
  keepAliveIntervalMs?: number; // default 300000 (5 min) or one third of the relay's session timeout; clamped to [60000, 600000]
  unencryptedHeaders?: string[]; // default [] (encrypt every header); ["*"] sends every header plain
  credentials?: RequestCredentials; // default "include"
  warnings?: boolean; // default true; set false to silence SDK console warnings
  requestStreaming?: boolean | { minBytes?: number }; // default true; see Streamed request bodies
  streamDiagnostics?:
    | boolean
    | {
        maxPreviewBytes?: number; // default 48
        onEvent?: (event: StreamDiagnosticEvent) => void;
      };
  onPoolEvent?: (event: PoolEvent) => void;
};

type MteFetchOptions = {
  encodeType?: "MTE" | "MKE";
  unencryptedHeaders?: string[]; // replaces the init list for this request only
  requestStreaming?: boolean; // false stages this body; true streams it whatever its size
};

Notes:

  • Requests are routed to relay sessions by request URL origin and effective encode type (for example, https://relay.example.com/... with the default "MKE" mode uses a different internal session than an explicit per-request "MTE" override).
  • Request URLs must resolve to a normal HTTP(S) origin. Requests that do not produce one throw an error.
  • httpTimeoutMs must be a positive integer.
  • sequenceWindow must be an integer in the range -65535..65535. When you do not set it and the relay reports its own value through discovery, the SDK adopts the relay's value.
  • timeWindow must be a non-negative integer.
  • companyName and licenseKey are required for license bootstrap.
  • minPairs controls the warm baseline pool size. Background refill fires when idle pairs drop to or below this value. Must be a positive integer; default 5.
  • initialPairs is how many pairs are created when a session starts. Must be an integer between minPairs and maxPairs; default 8.
  • maxPairs is the hard ceiling on the pool. When the pool is at this count and no pair is idle, mteFetch throws MteRelayCapacityError immediately. Must be an integer ≥ minPairs; default 15. If the relay advertises a lower per client cap through discovery, the SDK clamps all three pool values to it and warns once.
  • Framed MKE responses stream without special configuration when the effective encode type is "MKE", which is the default.
  • Requests that explicitly set encodeType: "MTE" throw a protocol error if the relay returns an MKE-framed response.
  • keepAliveIntervalMs controls how often the SDK pings /api/mte-keepalive to keep its pair pool from being reaped by the relay server's session timeout. Each relay session runs its own timer once it has paired. In browsers, an immediate ping also fires on document.visibilitychange → visible to recover from background-tab throttling. Must be an integer in [60000, 600000] ms (1–10 minutes); default 300000 (5 minutes). When you do not set it and the relay reports its session timeout through discovery, the SDK uses one third of that timeout, clamped to the same range. When you do set it above one third of the relay's timeout, the SDK warns once.
  • unencryptedHeaders lists caller header names that travel as ordinary HTTP headers on the relay POST instead of inside the encrypted metadata. Default is an empty list, so every header is encrypted. See Header encryption below.
  • credentials is the fetch credentials mode the SDK applies to every request it makes to the relay: auth, pair, keepalive, and framed requests. Default "include". Must be "include", "same-origin", or "omit".
  • warnings turns the SDK's once per session console warnings on or off. Default true.
  • streamDiagnostics is optional and disabled by default. Set it to true for bounded console diagnostics, or provide { maxPreviewBytes, onEvent } to capture native SSE chunks and MKE decrypt chunk previews programmatically. Every request-frame event carries details.transport, "stream" or "buffer", so a caller can see which path a body took.
  • onPoolEvent is optional. When provided, it is called for pool lifecycle events: refill-triggered, pair-appended, pair-removed, pair-replaced, capacity-exhausted, stream-reserved, and stream-released.
  • requestStreaming turns streamed request bodies on or off. Default true. See Streamed request bodies below.

Streamed request bodies

A request body can travel in two ways. The staged path frames the whole body before the POST: every DATA frame is encoded, the frames are joined into one buffer (or, above 32 MB, into blobs), and the POST carries a Content-Length. The streamed path hands fetch a ReadableStream with duplex: "half": the REQUEST frame goes out on the first pull and one DATA frame is cut, encoded, and framed on each pull after that. Nothing is staged, a body of any size costs a few chunks of memory, and the relay starts reading the first frames while the last are still being encoded.

Streaming is a capability, not a request. The SDK uses it only when three things hold, and falls back to the staged path silently otherwise:

  • The runtime can send a ReadableStream body. Chromium 105 and later (Chrome, Edge, Opera) and Node can; Firefox and Safari have not shipped it. supportsStreamingUpload() answers this without network traffic by constructing a Request and checking that the runtime read the duplex member and did not stringify the stream.
  • The path to the relay carries it. Chromium sends a streamed body over HTTP/2 or HTTP/3 only and rejects the request on an HTTP/1.1 connection, and something in front of the relay may demand a Content-Length. The SDK verifies each relay origin once per session with a streamed POST of three bytes to /api/mte-echo. Any HTTP answer, including the echo's 405, proves the body was delivered; a rejection before any answer marks the origin unsupported; a timeout leaves it unverified and the next body probes again. A 411 from the path on a real request also marks the origin, and that request is restaged and sent again on a fresh pair.
  • The body is worth it. By default a body that fits one DATA frame is sent staged with a known length, which the browser can replay by itself on a stale connection, and a body that needs more than one frame streams. { minBytes: n } streams every body of n bytes or more instead, and { minBytes: 0 } streams every body that has one. A per request requestStreaming: true streams whatever the size; false stages that one body.

A caller may also pass a ReadableStream as the body itself. On a runtime and a relay that can carry it the stream is framed as it is read, with no size known up front, so the relay's maxRequestBodyBytes is enforced by the relay's 481 rather than before the send. Where streaming is off or unavailable the stream is read to its end and sent staged, never as the string "[object ReadableStream]". A caller's stream cannot be replayed, so a registry retry on such a request surfaces the error instead.

import {
  mteFetch,
  requestStreamingStatus,
  supportsStreamingUpload,
  verifyRequestStreaming,
} from "mte-relay-browser";

supportsStreamingUpload(); // true on Chromium 105+ and Node, false on Firefox and Safari

// Settle the verdict for a relay before the first upload, rather than on it.
const status = await verifyRequestStreaming("https://relay.example.com");
// { supported, enabled, minBytes, endpoint: "verified" | "unsupported" | "unverified",
//   evidence: "probe" | "response" | "none", active }

requestStreamingStatus("https://relay.example.com").active; // what the next large body will do

MteRelayClient exposes the same two as methods. The relay reads a streamed body exactly as it reads a staged one: one REQUEST, then DATA frames to END, forwarded upstream as they decode. A proxy in front of the relay that buffers request bodies still works; it only delays when the origin sees the first byte.

Public API

From src/mte-relay/index.ts:

  • initMteRelayClient(options)
  • mteFetch(resource, init?, mteOptions?)
  • supportsStreamingUpload(), requestStreamingStatus(relayOrigin?), verifyRequestStreaming(relayOrigin) — streamed request bodies, see the section above
  • MteRelayCapacityError — thrown when the pair pool is exhausted
  • MteRelayRequestTooLargeError — thrown before upload when a body exceeds the relay's advertised maxRequestBodyBytes; carries size and limit
  • PoolEvent / PoolEventType — types for the onPoolEvent callback

initMteRelayClient(options)

Initializes singleton bootstrap and configuration. Must be called before mteFetch. Per-server authentication and pairing occur lazily on first request to each server origin.

mteFetch(resource, init?, mteOptions?)

Fetch-compatible request API:

(
  resource: RequestInfo | URL,
  init?: RequestInit,
  mteOptions?: { encodeType?: "MTE" | "MKE"; unencryptedHeaders?: string[] },
) => Promise<Response>;

Requests must resolve to a normal HTTP(S) origin. The SDK does not support a configured relay URL fallback. The first two parameters stay fetch-compatible. MTE-specific request overrides live in the optional third argument so they do not pollute RequestInit. A per request unencryptedHeaders list replaces the init list for that call only and is validated before any network activity.

Quick Start

import { initMteRelayClient, mteFetch } from "mte-relay-browser";

await initMteRelayClient({
  companyName: "your-company",
  licenseKey: "your-license-key",
  defaultEncodeType: "MKE",
});

const response = await mteFetch("https://server-a.example.com/api/mte-echo", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message: "hello" }),
});

const statsResponse = await mteFetch("https://server-b.example.com/stats");

const strictMteResponse = await mteFetch(
  "https://server-b.example.com/audit",
  {
    method: "POST",
  },
  {
    encodeType: "MTE",
  },
);

Request / Response Behavior

  • Request method/path/headers/body are packed into an MTE frame.
  • Relay transport always uses:
    • Method: POST
    • Header: Content-Type: application/octet-stream, plus any caller headers listed in unencryptedHeaders, plus an X-MTE-Relay-Route routing token once the relay has issued one at auth (the client level token on auth, pair, and keepalive; the pair id on framed requests)
    • credentials as configured (default "include")
  • Requests whose path matches a pass through route advertised by the relay are sent as plain fetch calls with the configured credentials. No frame, no pair.
  • On response:
    • If relay returns native text/event-stream, SDK preserves the streaming body and returns it as a normal browser Response.
    • If payload starts with MTE, SDK decodes response metadata first.
    • If the framed response uses mteType=1, SDK returns a synthetic browser Response immediately and streams the body by running one progressive StartDecrypt / DecryptChunk / FinishDecrypt session across the response body bytes, including decoded text/event-stream responses.
    • Requests that explicitly use encodeType: "MTE" fail with a protocol error if the relay returns an MKE-framed response.
    • Otherwise, SDK buffers the remaining framed body, decodes it, and returns upstream status/headers/body.
    • If payload is non-MTE, SDK returns raw response payload as-is.

Header encryption

Every caller header is encrypted into the request metadata by default. unencryptedHeaders is a denylist of names that travel as ordinary HTTP headers on the relay POST instead.

await initMteRelayClient({
  companyName: "your-company",
  licenseKey: "your-license-key",
  unencryptedHeaders: ["traceparent", "x-request-id"], // default [] = encrypt everything
  credentials: "include", // default "include"
});

await mteFetch(url, init, { unencryptedHeaders: [] }); // per request override, replaces the init list
  • [] (the default) encrypts every header.
  • Listed names are removed from the encrypted set and sent plain.
  • ["*"] sends every header plain except the two reserved names below. "*" must be the only entry.
  • Names are trimmed and lower cased. Each must be a valid HTTP header token. Anything else throws at initMteRelayClient or, for a per request list, before any network activity.

Reserved names, always encrypted even under ["*"]. Listing either throws.

| Name | Why | | --- | --- | | content-type | The relay POST must carry application/octet-stream or the server does not parse the frame. | | x-mte-relay-route | The relay's sticky routing token. The SDK owns it; a caller must not put a value on the wire under this name. |

Facts to know:

  • Names are matched case insensitively. The server canonicalizes on receipt, so x-tenant-id arrives at the origin as X-Tenant-Id from either channel.
  • Forbidden browser header names (host, cookie, origin, referer, content-length, connection, accept-encoding, sec-*, and the rest of the Fetch standard's list) never reach the SDK. The browser drops them from new Request(...). Listing one is accepted and does nothing.
  • One value per name. Repeated Headers.append calls are joined by the browser into one comma separated value in both channels.
  • Size cap. The path and encrypted headers travel together as one JSON object inside the metadata section, and that section must fit in 65535 bytes once encoded. The bound on the JSON plaintext depends on the encode type, because standard MTE expands the plaintext about 8x while MKE adds only a few hundred bytes: MAX_METADATA_PLAINTEXT_BYTES_MKE is 61439 and MAX_METADATA_PLAINTEXT_BYTES_MTE is 7168 (both exported from the protocol constants and mirroring the server's bounds). The SDK checks the JSON byte count against the bound for the request's encode type before encoding. Overflow throws a non repairable MteRelayProtocolError naming the section, its size, the bound, and the encode type; nothing is truncated and no pair is replaced.
  • Sending a header plain does not mean the origin receives it. The relay forwards plain hop headers to the origin only by its per domain forward_plain_headers list (default none) and forwards cookies by forward_browser_cookies (default on). Encrypted always wins on a name collision at the server. The SDK warns once per name when discovery or the per request x-mte-plain-forwarded signal shows a plain header was not forwarded.
  • CORS. The relay POST is already preflighted because application/octet-stream is not a safelisted content type. When cors_origins is set on the relay domain, cors_allowed_headers must list Content-Type, X-MTE-Relay-Route, and every plain header name, because the configured list replaces the server default rather than extending it. Browsers ignore Access-Control-Allow-Headers: * on credentialed requests, so a wildcard does not help. If the operator overrides cors_exposed_headers, it must include X-MTE-Relay-Route or the SDK cannot read the routing token.
  • Cookies. Page JavaScript cannot read HttpOnly cookies or set Cookie, so cookies never enter the frame. The browser attaches them to the relay POST when credentials allows it. "include" (the default) is required for any relay that is not same origin with the page. Cookies only stick when the relay host is same site with the page (a subdomain); cross site relay hosts hit third party cookie blocking in Safari and Chrome, and no relay setting changes that.
  • Security. Plain headers, including authorization or anything under ["*"], are protected only by TLS on the client to relay hop and are readable and writable by any TLS terminating proxy or load balancer in front of the relay. Encrypted headers are not.

Verification recipe: point mteFetch at the jsonplaceholder /echo endpoint through the relay and read the X-Echo-Headers response header, which is the origin's view of the request headers as JSON. Compare with the relay POST in devtools.

Relay discovery

GET /api/mte-relay may return a relay object next to clientId. The SDK reads it at session init and on every repair, so relay config changes are picked up the next time the session re authenticates. Every field is optional. A relay that returns none behaves exactly as before.

| Field | What the SDK does | | --- | --- | | protocolVersion | Throws at session init if the relay speaks a newer frame protocol than this SDK (currently 1). | | encodeTypes | Throws at session init if the session's encode type is not accepted, listing the accepted types. | | kyberStrength | Throws at session init if it differs from the MTE build's Kyber strength. | | sequenceWindow | Adopted when you did not set sequenceWindow. Warns once if you set a different value. | | sessionTimeoutSeconds | Keepalive interval becomes one third of the timeout (clamped to 1 to 10 minutes) when you did not set keepAliveIntervalMs. Warns once if your value exceeds one third. | | maxRequestBodyBytes | Bodies over the limit throw MteRelayRequestTooLargeError before anything is sent. This is a pre filter on the raw body; the relay applies the limit to the whole frame, so a body just under the limit can still get a 413 once metadata and MTE overhead are added. | | maxPairsPerClient | Clamps maxPairs, initialPairs, and minPairs to the cap and warns once. | | passThroughRoutes | Matching paths (exact, or prefix when the entry ends in *) are sent as plain fetch calls. A credentials value in the request init wins; otherwise the configured mode applies. The route list arrives with the auth response, so the first request to a relay origin still authenticates and pairs before any pass through call. | | forwardPlainHeaders | Each name in unencryptedHeaders is checked against the list (* globs supported, ["*"] means all). Warns once per name that will not reach the origin. | | forwardBrowserCookies | Warns once when true and credentials is "omit". | | sseWriteDeadlineSeconds | A stream that fails after that much idle time gets an error explaining that the relay closes idle streams and the upstream must send a heartbeat sooner. | | configVersion, serverVersion | Logged at session init and when configVersion changes between auths. |

Warnings go through console.warn, at most once per session per subject. Pass warnings: false to silence them.

Error Handling

MteRelayError.requestId and MteRelayTransportError.requestId preserve the response's X-MTE-Relay-Request-Id, including failures while reading a stream. Search server logs for the same value in correlationId. The relay generates an ID when no tracing ID was supplied; the SDK leaves requestId null when none was received.

Relay HTTP 559–569 throws an error object with:

  • statusCode: relay status code
  • responseText: plain text relay body
  • repaired: whether repair flow was executed

Behavior:

  • Relay HTTP 559–569 → log the relay status, drop all pairs for that origin, re-authenticate, re-pair, then throw a repaired error without retrying the original request
  • Local MTE encode/decode or frame parsing failures → log the MTE status when available, drop all pairs for that origin, re-authenticate, re-pair, then throw a repaired MteRelayProtocolError
  • Pool capacity exhaustion (maxPairs reached with no idle pair) → throw MteRelayCapacityError immediately; repair proceeds in the background if needed
  • Body larger than the relay's advertised maxRequestBodyBytes → throw MteRelayRequestTooLargeError before any upload; the pool is untouched
  • Request metadata JSON (path plus encrypted headers) larger than the bound for the encode type (61439 bytes for MKE, 7168 for MTE) → throw MteRelayProtocolError naming the section, size, bound, and encode type before any MTE encode; the pool is untouched
  • Structural response frame errors (length mismatch, truncated section, zero length metadata, metadata that is not the expected JSON object) → throw a non repairable MteRelayProtocolError; these are deterministic, so the pool is untouched and no pair is replaced

MteRelayCapacityError

Thrown when the pair pool is at maxPairs and no encoder or decoder is immediately available. It is distinguishable from relay HTTP errors and protocol errors by checking instanceof MteRelayCapacityError.

import { MteRelayCapacityError } from "mte-relay-browser";

try {
  await mteFetch("https://api.example.com/data");
} catch (error) {
  if (error instanceof MteRelayCapacityError) {
    console.error(`Pool exhausted at maxPairs=${error.maxPairs}. Retry later.`);
    return;
  }

  if (
    typeof error === "object" &&
    error !== null &&
    "statusCode" in error &&
    "repaired" in error
  ) {
    console.error(error);
    // decide whether and when to retry
  }
}

Concurrency Model

  • Browser runtime is single-threaded.
  • SDK maintains a pair pool for each origin plus encode type in use, starting with initialPairs pairs (default 8) and growing up to maxPairs (default 15).
  • Requests select idle pairs round-robin using internal request counter.
  • Each pair has a serialized queue to preserve MTE state ordering, and queued or in-flight work makes that pair busy for new selection.
  • When idle pair count drops to or below minPairs, the SDK starts a background refill of up to minPairs additional pairs (bounded by maxPairs).
  • When all pairs are busy and the pool is below maxPairs, the SDK triggers a background refill and waits for a pair to become available.
  • When all pairs are busy and the pool is already at maxPairs, mteFetch throws MteRelayCapacityError immediately.
  • Requests expecting SSE reserve their selected pair until the response stream completes, errors, or is canceled. Reserving a pair immediately triggers a background refill to maintain spare capacity.
  • Different pairs can progress concurrently across asynchronous requests.

Troubleshooting

Pairing fails (/api/mte-pair)

  • Confirm relay URL is correct.
  • Check relay server logs for request validation errors.
  • Ensure client/server clock skew and session handling are healthy.
  • If a dynamic append attempt fails while the pool is saturated, the SDK repairs the affected session before surfacing the append failure.

Frequent relay errors 559/564

  • Session expiration or state drift is likely.
  • SDK logs the relay status, performs a full re-pair for that origin, and then surfaces the repaired error.
  • Caller should decide whether to retry based on endpoint semantics.

Local MTE encode/decode failures

  • These failures also trigger a full per-origin re-pair.
  • Check the browser console for the logged MTE status code or frame offset details.

Non-MTE response returned

  • Relay may have returned plain-text error or non-frame response.
  • SDK intentionally returns raw response in that case.

SSE request does not stream

  • Confirm the decoded upstream response header is Content-Type: text/event-stream.
  • If the relay returns SSE inside an MKE frame, confirm the decoded metadata reports mteType=1 and the request's effective encode type is "MKE".
  • MKE-framed SSE uses the same progressive StartDecrypt / DecryptChunk / FinishDecrypt path as other MKE-framed response bodies.
  • Requests that explicitly set encodeType: "MTE" fail with a protocol error if the relay returns an MKE-framed SSE response.

Inspecting SSE chunk bytes

Enable bounded stream diagnostics when you need to prove whether JSON escape bytes stay intact through the SDK boundary:

import {
  initMteRelayClient,
  type StreamDiagnosticEvent,
} from "mte-relay-browser";

await initMteRelayClient({
  companyName: "your-company",
  licenseKey: "your-license-key",
  streamDiagnostics: {
    maxPreviewBytes: 64,
    onEvent(event: StreamDiagnosticEvent) {
      if (
        event.channel === "native-event-stream" ||
        event.channel === "mke-progressive"
      ) {
        console.log("[stream diagnostic]", event);
      }
    },
  },
});

The diagnostic event includes:

  • hexPreview: bounded byte preview such as 20 20 5c 6e versus 20 20 0a
  • escapedPreview: byte-oriented escaped text preview such as \\n versus \n
  • stage: whether the bytes were seen at native relay ingress, MKE encrypted chunk ingress, decrypted chunk output, or finishDecrypt

Use the SDK preview together with consumer-side JSON.stringify(event.data) checks. If the SDK logs 5c 6e / \\n but the browser event payload later shows 0a / \n, the corruption happened after this package handed bytes to the browser Response stream.

Non-SSE MKE response buffers unexpectedly

  • Confirm the decoded frame metadata uses mteType=1.
  • Confirm the request's effective encode type is "MKE" so the client has an MKE decoder available.
  • Non-SSE MKE responses stream by feeding each arriving transport chunk through one StartDecrypt / DecryptChunk / FinishDecrypt session.
  • Only non-MKE framed responses stay on the buffered whole-body decode path.

Periodic POSTs to /api/mte-keepalive

  • Expected. Each active relay session pings every keepAliveIntervalMs (default 5 minutes) so the server does not reap idle pairs.
  • The request is plain JSON (not an MTE frame) and carries the session's clientId plus its current pair IDs.
  • Network failures, timeouts, and 5xx responses are logged and ignored — the next tick retries naturally.
  • A 564 response (signed clientId failed HMAC verification) stops the loop, drops local pairs, and lets the next mteFetch lazily re-authenticate and re-pair.

Decode failures after custom modifications

  • Re-check cross-wiring rules in pairing.
  • Verify nonces are used verbatim (no masking or other transformation) and encode/decode operation ordering.

Plain header never reaches the origin

  • The name is not in the relay domain's forward_plain_headers list, or the relay predates that setting. The relay drops plain hop headers it is not told to forward.
  • Look for the SDK warning naming the header. It fires once per session, from discovery at init or from the per request x-mte-plain-forwarded signal.
  • Either ask the operator to add the name (globs like x-trace-* work) or remove it from unencryptedHeaders so it travels encrypted.

Request fails with a CORS error after adding unencryptedHeaders

  • The relay domain sets cors_allowed_headers, which replaces the server default instead of extending it, and the list does not include the new plain header name or is missing Content-Type or X-MTE-Relay-Route.
  • Access-Control-Allow-Headers: * does not fix it; browsers ignore the wildcard on credentialed requests.

Every cross origin request fails with a CORS error after upgrading

  • The SDK now sends every relay request with credentials: "include". A relay domain with cors_allow_credentials: false makes the browser reject those responses, where the previous SDK's same-origin default did not send credentials cross origin. Either leave the relay default (credentials allowed) or pass credentials: "same-origin" to the SDK.
  • A custom cors_allowed_headers list that omits X-MTE-Relay-Route fails the preflight once the relay has issued a routing token. Add it to the list.

Cookies never stick

  • The relay host is not same site with the page. Third party cookie blocking in Safari and Chrome drops them, and no relay setting changes that. Put the relay on a subdomain of the page's site.
  • credentials is "omit" or "same-origin" on a cross origin relay. The default "include" is required there.
  • The SDK warns once when the relay reports forwardBrowserCookies: true and credentials is "omit".

Session init throws about protocol version, encode type, or Kyber

  • The relay's discovery object reported a value this SDK or MTE build cannot satisfy. The message names both sides.
  • Protocol version: upgrade mte-relay-browser. Encode type: change defaultEncodeType or the per request encodeType. Kyber: the MTE WASM build must match the relay's KYBER_STRENGTH.

Security and Operational Notes

  • Do not log sensitive request/response bodies in production.
  • Treat clientId and cryptographic material as sensitive.
  • Prefer short transport timeouts with caller-managed retries.

Versioning and Changelog Policy

  • Follow Semantic Versioning for SDK releases.
    • Major: breaking API/protocol behavior
    • Minor: backward-compatible features
    • Patch: fixes and non-breaking improvements
  • Maintain CHANGELOG.md for each release with:
    • Added
    • Changed
    • Fixed
    • Security (if applicable)

Release Readiness Checklist

  • npm run build passes
  • npm run publish:beta:dry-run shows only dist/** package contents and no .map files
  • Beta publish uses npm publish --tag beta (or npm run publish:beta)
  • Public API changes documented in changelog