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
XMLHttpRequesttransport as well as those usingwithFetch(). - 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.
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:
- Bootstraps the MTE WASM runtime and validates your license.
- Authenticates with the relay (
GET /api/mte-relay) to obtain a signed client ID. - Performs a Kyber-1024 key exchange (
POST /api/mte-pair) to create a pool of paired encoders/decoders. - Encodes each request's method, path, query, headers, and body into an MTE binary frame, and decodes responses back into normal Angular
HttpResponseobjects. - Maintains the session in the background: keep-alive pings, pool refills, and automatic repair after relay errors.
Requirements​
| Requirement | Notes |
|---|---|
| Angular | >=17.0.0 (peer range widens with each new Angular major) |
| MTE Relay Server | v5 only |
| MTE WASM library | The mte package build matched to your relay server's MTE library family. A client/server mismatch typically surfaces as relay error 562. |
| Registry access | The @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 forng buildto succeed. It is safe: the reference becomes an inert shim that browser code never calls.- Do not apply
externalDependenciesto a configuration used byng serve— the dev server handles the Node builtin itself and breaks if the module is marked external. allowedCommonJsDependenciesonly silences the CommonJS optimization warning and is optional.
Provider Ordering​
Both provideHttpClient() and provideMteRelay() bind Angular's HttpBackend token, and the last binding wins.
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:
| Option | Type / Default | Description |
|---|---|---|
companyName | string, required | MTE license company name. |
licenseKey | string, required | MTE license key. |
relayOrigins | (string | RegExp)[], required | Origins 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. |
minPairs | number, 5 | Idle-pair watermark that triggers a background pool refill. |
initialPairs | number, 8 | Encoder/decoder pairs created when a session starts. |
maxPairs | number, 15 | Hard ceiling on the pair pool. |
sequenceWindow | number, -63 | MTE decoder sequence window. |
timeWindow | number, 1000 | MTE decoder time window (ms). |
httpTimeoutMs | number, 30000 | Relay transport timeout. |
keepAliveIntervalMs | number, 300000 | Keep-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 default | Transport 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. |
autoRetry | boolean | object, true | Automatic single retry of idempotent requests after session repair. See Automatic Retry. |
eagerInit | boolean, false | Warm 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. |
streamDiagnostics | engine option | Bounded byte-level stream diagnostics (debugging aid). |
onPoolEvent | (event) => void | Pool 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'sFetchBackend.
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();
}
}
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:
| Failure | status | .error contains |
|---|---|---|
| Relay error 559–569 | the relay status code | MteRelayHttpError — includes repaired / pairReplaced flags describing the recovery already performed |
| Local MTE protocol failure | 0 | MteRelayProtocolError |
| Pair pool exhausted | 0 | MteRelayCapacityError |
| Upstream non-2xx | the upstream status | the 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-0HttpErrorResponseduring 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
onPoolEventandstreamDiagnosticscallbacks fire outside the zone; wrap any state updates inNgZone.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. MteEventSourcedoes not auto-reconnect or replayLast-Event-ID(planned).- MTE Relay Server v5 only.
Troubleshooting​
| Symptom | Likely cause / fix |
|---|---|
Startup error naming provideMteRelay() and HttpBackend | Provider 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 562 | Client/server MTE library mismatch — install the mte package build matched to your relay server. |
MteRelayCapacityError | Pair pool exhausted, usually by many concurrent streams. Raise maxPairs or reduce concurrent streams. |
| License error in the console at startup | Verify 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 tab | The origin did not match relayOrigins (check exact scheme/host/port), or the request set MTE_BYPASS. |