Skip to main content
Version: 5.0.x 🚧

MTE Relay Client for Angular

MTE Relay Client for Angular​

The MTE Relay client library for Angular, mte-relay-angular, is a drop-in integration for Angular's HttpClient. Instead of asking you to replace every HTTP call with a special function, it replaces the transport layer underneath HttpClient — so your existing services, interceptors, error handling, and anything else built on Angular's HTTP stack keep working unchanged, while requests to your MTE Relay server are transparently encrypted end to end.

  • Supports Angular 17 through the current release, standalone and NgModule bootstrap, zone.js and zoneless apps.
  • Supports applications using the default XMLHttpRequest transport as well as those using withFetch().
  • Handles the complete MTE session lifecycle automatically: WASM/license bootstrap, client authentication, quantum-resistant Kyber key exchange, encoder/decoder pair pools, background pool refills, session repair, and keep-alive pings.
Server version

This client targets MTE Relay Server v5 (binary MTE protocol) only. If you are running a v4 relay server, use the JavaScript client instead.

Quick Start​

1. Install the packages. Add the Eclypses registry for the MTE WASM library to your project's .npmrc, then install:

@eclypses:registry=https://npm.eclypses.com
npm install mte-relay-angular
npm install mte@npm:@eclypses/<your-matched-mte-build>@^4.2.1

2. Add one provider to your bootstrap — after provideHttpClient():

// main.ts
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideMteRelay } from 'mte-relay-angular';

bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideMteRelay({
companyName: 'your-company',
licenseKey: 'your-license-key',
relayOrigins: ['https://relay.example.com'],
}),
],
});

3. Use HttpClient exactly as you already do.

@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);

getUsers() {
// Transparently MTE-encrypted — no call-site changes.
return this.http.get<User[]>('https://relay.example.com/users');
}
}

That's it. Open your browser's network tab and you will see requests to the relay origin sent as encrypted binary frames (POST / with Content-Type: application/octet-stream), while requests to every other origin behave exactly as before.


Detailed Documentation​

How It Works​

Angular sends every HttpClient request through an interceptor chain that terminates in an HttpBackend. provideMteRelay() replaces that backend:

HttpClient
│
â–¼
your interceptors (unchanged — they see plaintext)
│
â–¼
MteRelayBackend ──── origin NOT in relayOrigins ───► normal transport
│ (XHR or fetch)
origin in relayOrigins
│
â–¼
MTE Relay engine (WASM, Kyber pairing, pair pool, session repair)
│
â–¼
POST / (application/octet-stream) to your relay server

Because interceptors run above the backend, your authentication headers, logging, and error-mapping interceptors continue to work — they operate on plaintext requests and decoded responses; only the wire transport changes.

On the first request to a relay origin the client automatically:

  1. Bootstraps the MTE WASM runtime and validates your license.
  2. Authenticates with the relay (GET /api/mte-relay) to obtain a signed client ID.
  3. Performs a Kyber-1024 key exchange (POST /api/mte-pair) to create a pool of paired encoders/decoders.
  4. Encodes each request's method, path, query, headers, and body into an MTE binary frame, and decodes responses back into normal Angular HttpResponse objects.
  5. Maintains the session in the background: keep-alive pings, pool refills, and automatic repair after relay errors.

Requirements​

RequirementNotes
Angular>=17.0.0 (peer range widens with each new Angular major)
MTE Relay Serverv5 only
MTE WASM libraryThe mte package build matched to your relay server's MTE library family. A client/server mismatch typically surfaces as relay error 562.
Registry accessThe @eclypses npm scope must resolve to https://npm.eclypses.com

Build Configuration​

The MTE WASM library contains a Node-only require("crypto") behind a runtime guard that never executes in browsers, but the Angular production builder still tries to resolve it. Add the following to your application's builder options in angular.json:

{
"options": {
"allowedCommonJsDependencies": ["mte"]
},
"configurations": {
"production": {
"externalDependencies": ["crypto"]
}
}
}
  • externalDependencies: ["crypto"] is required for ng build to succeed. It is safe: the reference becomes an inert shim that browser code never calls.
  • Do not apply externalDependencies to a configuration used by ng serve — the dev server handles the Node builtin itself and breaks if the module is marked external.
  • allowedCommonJsDependencies only silences the CommonJS optimization warning and is optional.

Provider Ordering​

Both provideHttpClient() and provideMteRelay() bind Angular's HttpBackend token, and the last binding wins.

warning

provideMteRelay() must be listed after provideHttpClient(). The library verifies this at startup and fails with a descriptive error if the order is wrong — a misconfigured application will refuse to start rather than silently sending plaintext.

Once installed, the library owns the transport for all traffic. withFetch() on provideHttpClient() has no effect; use the fallbackTransport option (below) to choose the transport for non-relay traffic.

Configuration Reference​

provideMteRelay(config) accepts:

OptionType / DefaultDescription
companyNamestring, requiredMTE license company name.
licenseKeystring, requiredMTE license key.
relayOrigins(string | RegExp)[], requiredOrigins running MTE Relay v5. Strings are normalized to their origin. RegExps are tested against the resolved, lowercased origin and must be anchored (for example /^https:\/\/relay-\d+\.example\.com$/); unanchored patterns match substrings of other origins and log a warning. There is deliberately no "relay everything" mode.
defaultEncodeType'MTE' | 'MKE', 'MKE'Encoding mode for relayed requests. MKE is recommended and supports streamed responses.
minPairsnumber, 5Idle-pair watermark that triggers a background pool refill.
initialPairsnumber, 8Encoder/decoder pairs created when a session starts.
maxPairsnumber, 15Hard ceiling on the pair pool.
sequenceWindownumber, -63MTE decoder sequence window.
timeWindownumber, 1000MTE decoder time window (ms).
httpTimeoutMsnumber, 30000Relay transport timeout.
keepAliveIntervalMsnumber, 300000Keep-alive ping interval (clamped to 1–10 minutes). Keep it at or below one third of the server's session timeout.
ssr'passthrough' | 'block', 'passthrough'Behavior during server-side rendering. See Server-Side Rendering.
fallbackTransport'xhr' | 'fetch', framework defaultTransport for non-relay traffic. When omitted, mirrors the running Angular major's own default backend (XHR through Angular 21, fetch from Angular 22). See Passthrough Transport.
autoRetryboolean | object, trueAutomatic single retry of idempotent requests after session repair. See Automatic Retry.
eagerInitboolean, falseWarm up the WASM runtime at application startup instead of on the first relayed request. A failed warm-up logs and falls back to lazy initialization — it never blocks bootstrap.
streamDiagnosticsengine optionBounded byte-level stream diagnostics (debugging aid).
onPoolEvent(event) => voidPool lifecycle callback. Called outside the Angular zone.

Passthrough Transport​

Requests that do not target a relay origin — plus requests with the MTE_BYPASS token — are delegated to a normal Angular backend. Because there is no public API to detect whether your app used withFetch(), the library owns this choice:

When fallbackTransport is omitted, the default mirrors the running framework's own default backend — XHR on Angular 17–21, fetch on Angular 22+ (where FetchBackend became Angular's default) — so non-relay traffic behaves as if the library were not installed. To pin one explicitly:

  • 'xhr' — preserves upload-progress events for non-relay traffic.
  • 'fetch' — uses Angular's FetchBackend.

During SSR the passthrough is always fetch-based, since XMLHttpRequest does not exist on the server.

Per-Request Options​

Per-request behavior is controlled the Angular way — with HttpContext tokens:

import { HttpContext } from '@angular/common/http';
import { MTE_BYPASS, MTE_ENCODE_TYPE, MTE_RETRY_UNSAFE } from 'mte-relay-angular';

// Use strict MTE encoding (instead of the MKE default) for one request:
this.http.post(url, body, {
context: new HttpContext().set(MTE_ENCODE_TYPE, 'MTE'),
});

// Send one request to a relay origin WITHOUT the relay transport:
this.http.get(url, {
context: new HttpContext().set(MTE_BYPASS, true),
});

// Allow automatic retry for one non-idempotent request:
this.http.post(url, body, {
context: new HttpContext().set(MTE_RETRY_UNSAFE, true),
});

Automatic Retry​

Relay pair-state errors (HTTP 559–563 and local protocol failures) cause the engine to replace the failing pair — or fully re-authenticate and re-pair — before surfacing the error. By default the client then retries the request once, for idempotent methods only (GET, HEAD, OPTIONS). The retry lands on the already-repaired session, so a transient pair expiry does not become a user-visible failure.

Mutating methods (POST, PATCH, ...) are never retried automatically: relay error 563 is raised while encoding the response, meaning the upstream request may already have executed. Only your application can judge whether replay is safe.

provideMteRelay({
// ...
// Extend the idempotent list if your APIs treat PUT/DELETE as idempotent:
autoRetry: { idempotentMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'] },
// Or disable entirely:
// autoRetry: false,
});

Use the MTE_RETRY_UNSAFE context token (shown above) to opt in a single mutating request.

Streaming and Server-Sent Events​

Native EventSource cannot connect through an MTE Relay — relay traffic is binary POST frames. Two supported paths replace it:

1. HttpClient streaming events. Decoded relay streams (MKE progressive bodies, including SSE) surface exactly like stock Angular streaming responses:

this.http
.request('GET', url, {
observe: 'events',
responseType: 'text',
reportProgress: true,
})
.subscribe((event) => {
if (event.type === HttpEventType.DownloadProgress) {
console.log((event as HttpDownloadProgressEvent).partialText);
}
});

2. MteEventSource — an Observable replacement for EventSource with parsed events:

import { MteEventSource } from 'mte-relay-angular';

@Component({ /* ... */ })
export class TickerComponent {
private sse = inject(MteEventSource);

ngOnInit() {
this.subscription = this.sse
.connect('https://relay.example.com/sse/ticker')
.subscribe((event) => {
console.log(event.type, event.data, event.id);
});
}

ngOnDestroy() {
// Unsubscribing closes the stream and aborts the relay request.
this.subscription.unsubscribe();
}
}
Streaming limits

Each active stream reserves one encoder/decoder pair for its whole lifetime. Many concurrent streams can exhaust the pool (maxPairs, default 15) and fail with MteRelayCapacityError — budget maxPairs accordingly. MteEventSource currently does not auto-reconnect and does not replay Last-Event-ID; resubscribe to reconnect.

Error Handling​

Every failure surfaces as a standard HttpErrorResponse, so existing error interceptors keep working. Relay-specific failures carry a typed error object in .error:

Failurestatus.error contains
Relay error 559–569the relay status codeMteRelayHttpError — includes repaired / pairReplaced flags describing the recovery already performed
Local MTE protocol failure0MteRelayProtocolError
Pair pool exhausted0MteRelayCapacityError
Upstream non-2xxthe upstream statusthe decoded upstream body (identical to stock HttpClient)
import {
MteRelayCapacityError,
MteRelayHttpError,
} from 'mte-relay-angular';

catchError((err: HttpErrorResponse) => {
if (err.error instanceof MteRelayCapacityError) {
// Pair pool exhausted — back off and retry later.
}
if (err.error instanceof MteRelayHttpError) {
// err.status is the relay code (559–569).
// err.error.repaired / err.error.pairReplaced report the recovery
// the client already performed; a manual retry is usually safe for
// idempotent operations.
}
return throwError(() => err);
});

Server-Side Rendering (SSR)​

The MTE engine is browser-only (WASM + WebCrypto). During server rendering the backend never touches the engine and instead follows the ssr option:

  • 'passthrough' (default) — relay-origin requests are sent as plaintext directly to the target during SSR. Include this in your threat model, or use 'block'.
  • 'block' — relay-origin requests fail fast with a status-0 HttpErrorResponse during SSR; the browser makes the encrypted request after hydration.

Zones and Change Detection​

  • All engine work — WASM, pairing, keep-alive timers, stream pumps — runs outside the Angular zone. It never blocks application stability (SSR serialization, hydration) and never triggers gratuitous change detection.
  • Response events are delivered back in the zone you subscribed from, so change detection works normally in zone.js apps. Zoneless applications work naturally through observable emissions.
  • The onPoolEvent and streamDiagnostics callbacks fire outside the zone; wrap any state updates in NgZone.run() if they must trigger change detection.

NgModule-Based Applications​

Standalone bootstrap is not required. For NgModule apps, import the module after your HTTP setup:

@NgModule({
imports: [
BrowserModule,
HttpClientModule,
MteRelayModule.forRoot({
companyName: 'your-company',
licenseKey: 'your-license-key',
relayOrigins: ['https://relay.example.com'],
}),
],
})
export class AppModule {}

Testing Your Application​

Use provideMteRelayTesting() from the secondary entry point. It is a pure bypass: relay routing is disabled, HttpBackend is not replaced, and Angular's standard HTTP testing works untouched. The WASM runtime is never loaded in tests.

import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { provideMteRelayTesting } from 'mte-relay-angular/testing';

TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(), // HttpTestingController works as usual
provideMteRelayTesting(),
],
});

To unit-test your relay-error handling, build realistic errors with the included factories:

import { createMteRelayHttpErrorResponse } from 'mte-relay-angular/testing';

httpSpy.get.and.returnValue(
throwError(() => createMteRelayHttpErrorResponse(562, { pairReplaced: true })),
);

Known Limitations​

  • No upload-progress events for relayed requests. The engine MTE-encodes the entire request body before transmitting, so no progress exists during the encode phase, and transmit-phase progress is not currently implemented. This matches Angular 22+'s default FetchBackend, which also does not support upload progress. Non-relay traffic on the 'xhr' passthrough still receives upload progress.
  • MteEventSource does not auto-reconnect or replay Last-Event-ID (planned).
  • MTE Relay Server v5 only.

Troubleshooting​

SymptomLikely cause / fix
Startup error naming provideMteRelay() and HttpBackendProvider ordering — move provideMteRelay() after provideHttpClient().
ng build fails with Could not resolve "crypto"Add externalDependencies: ["crypto"] to your production build configuration. See Build Configuration.
Repeated relay error 562Client/server MTE library mismatch — install the mte package build matched to your relay server.
MteRelayCapacityErrorPair pool exhausted, usually by many concurrent streams. Raise maxPairs or reduce concurrent streams.
License error in the console at startupVerify companyName / licenseKey. With eagerInit, a failed warm-up is logged and retried lazily on the first relayed request.
Requests to the relay origin appear as plain JSON in the network tabThe origin did not match relayOrigins (check exact scheme/host/port), or the request set MTE_BYPASS.