mte-relay-angular
v0.1.0-beta.1
Published
Drop-in Angular HttpClient integration for Eclypses MTE Relay v5. Replaces the HttpBackend so existing HttpClient code and interceptors transparently use the MTE Relay binary protocol.
Readme
mte-relay-angular
Drop-in Angular integration for Eclypses MTE Relay v5.
Add one provider to your bootstrap and your existing HttpClient code — services, interceptors, error handling, httpResource, everything built on Angular's HTTP stack — transparently carries its traffic to configured relay origins over the MTE Relay binary protocol (MTE/MKE encoding, Kyber-1024 key exchange, pair pools, automatic session repair). Traffic to every other origin is untouched.
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideMteRelay } from 'mte-relay-angular';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
// MUST come AFTER provideHttpClient — see "Provider ordering" below.
provideMteRelay({
companyName: 'your-company',
licenseKey: 'your-license-key',
relayOrigins: ['https://relay.example.com'],
}),
],
});No call-site changes:
// This service is unmodified — requests to relay.example.com are now
// MTE-encoded end to end; requests to other origins behave exactly as before.
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers() {
return this.http.get<User[]>('https://relay.example.com/users');
}
}Supported: Angular 17 through current (peer range >=17 <23, widened after each new Angular major is verified) · zone.js and zoneless apps · standalone and NgModule bootstrap · XHR and fetch passthrough transports · MTE Relay server v5 only (mte-relay, the Go rewrite). There is no v4 support.
How it works
Angular's HttpClient sends every request through an interceptor chain that terminates in an HttpBackend. This library replaces that backend:
HttpClient → your interceptors (unchanged) → MteRelayBackend
│
origin in relayOrigins?
│ │
yes no
│ │
MTE Relay engine passthrough backend
(mte-relay-browser: WASM, (HttpXhrBackend or
Kyber pairing, pair pool, FetchBackend, per
binary frames, repair, fallbackTransport)
keep-alive, MKE streaming)
│
POST / (application/octet-stream)
to the relayBecause interceptors run above the backend, your auth headers, logging, retry and error-mapping interceptors keep working — they see plaintext requests and decoded responses; only the wire transport changes.
The cryptographic engine is mte-relay-browser (≥ 5.0.0-beta.7), used as an instance-based dependency. This package adds no protocol logic of its own; it adapts HttpRequest/HttpEvent semantics to the engine and manages Angular concerns (DI, zones, SSR, testing).
What a relayed request does
- First request to a relay origin lazily bootstraps the MTE WASM runtime and license, authenticates (
GET /api/mte-relay→ signedclientId), and creates a pool of Kyber-paired encoder/decoder pairs (POST /api/mte-pair). - The request method, path, query, headers, and body are packed and MTE/MKE-encoded into a binary frame, POSTed to the relay origin as
application/octet-stream. - The response frame is decoded (progressively, for MKE streams) and surfaced as a normal Angular
HttpResponse/HttpEventsequence. - The engine maintains keep-alive pings, refills the pair pool in the background, and repairs sessions after relay errors (HTTP 559–569).
Installation
npm install mte-relay-angularTwo peer requirements beyond Angular itself:
The MTE WASM library (
mte, an npm alias to a private@eclypses/...package). This package is bring-your-own-WASM: install themtebuild that matches your relay server's library family (client and server libraries must match — a mismatch typically manifests as relay error 562). Your.npmrcneeds the Eclypses registry for@eclypsesscope:@eclypses:registry=https://npm.eclypses.comnpm install mte@npm:@eclypses/<your-matched-mte-build>@^4.2.1mte-relay-browser≥ 5.0.0-beta.7 — installed automatically as a dependency.
Required app configuration: the crypto reference
The MTE WASM library contains a Node-only require("crypto") behind a runtime guard that never executes in browsers. The Angular production builder still tries to resolve it. Add this to your application's angular.json build options (production configuration is sufficient; ng serve does not need it):
"options": {
"allowedCommonJsDependencies": ["mte"] // silences the CJS optimization warning
},
"configurations": {
"production": {
"externalDependencies": ["crypto"] // required for ng build
}
}Without externalDependencies, ng build fails with Could not resolve "crypto". This is safe: the reference becomes an inert shim that the browser code path never calls. Do not put externalDependencies on a configuration used by ng serve — the vite dev server handles the builtin itself and breaks if the module is marked external.
Provider ordering (important)
Both provideHttpClient() and provideMteRelay() bind HttpBackend, and the last binding wins. provideMteRelay() must come after provideHttpClient() or no traffic is protected. The library asserts this at startup and fails with a descriptive error if the order is wrong — a misconfigured app will not silently send plaintext.
For NgModule-based apps:
@NgModule({
imports: [
BrowserModule,
HttpClientModule, // or provideHttpClient in providers
MteRelayModule.forRoot({ companyName: '...', licenseKey: '...', relayOrigins: ['...'] }),
],
})
export class AppModule {}Note: once installed, this library owns the transport. withFetch() on provideHttpClient() becomes inert; use the fallbackTransport option instead to choose the passthrough transport.
Configuration reference
provideMteRelay({
// Required
companyName: string, // MTE license company
licenseKey: string, // MTE license key
relayOrigins: (string | RegExp)[], // origins running MTE Relay v5.
// Strings normalize to their origin; RegExps
// test against the resolved, lowercased
// origin and MUST be anchored, e.g.
// /^https:\/\/relay-\d+\.example\.com$/ —
// unanchored patterns match substrings of
// other origins and log a warning.
// Mandatory allowlist — no "relay everything".
// Engine tuning (defaults from mte-relay-browser)
defaultEncodeType?: 'MTE' | 'MKE', // default 'MKE'
minPairs?: number, // default 5 — refill watermark
initialPairs?: number, // default 8 — pairs created at session start
maxPairs?: number, // default 15 — hard pool ceiling
sequenceWindow?: number, // default -63
timeWindow?: number, // default 1000 (ms)
httpTimeoutMs?: number, // default 30000
keepAliveIntervalMs?: number, // default 300000; clamped 60000–600000
// Angular-side behavior
ssr?: 'passthrough' | 'block', // default 'passthrough' (see SSR below)
fallbackTransport?: 'xhr' | 'fetch', // default: framework default (see below)
autoRetry?: boolean | { idempotentMethods?: string[] },
// default true (see Auto-retry below)
eagerInit?: boolean, // default false — warm up WASM at app start
streamDiagnostics?: ..., // engine diagnostics passthrough
onPoolEvent?: (event) => void, // pool lifecycle; called OUTSIDE the zone
})fallbackTransport
Requests that do not target a relay origin are delegated to a normal backend. There is no public API to detect whether the app used withFetch(), so this library owns the choice.
When fallbackTransport is omitted, the default mirrors the running framework's own default HttpBackend, so passthrough traffic behaves as if this library were not installed: XHR on Angular 17–21, fetch on Angular 22+ (where FetchBackend became Angular's default and withFetch() was deprecated). Set 'xhr' or 'fetch' explicitly to pin one — 'xhr' preserves upload-progress events for passthrough traffic. During SSR the passthrough is always FetchBackend regardless of this setting — XHR does not exist on the server.
Auto-retry
Relay pair-state errors (HTTP 559–563, and local protocol failures) cause the engine to replace the failing pair or fully repair the session, then surface the error. By default this library 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 surface as a user-visible failure.
Mutating methods are never retried automatically: relay error 563 is raised while encoding the response, which means the upstream request may already have executed; only the caller can judge replay safety. Options:
- Extend the idempotent list:
autoRetry: { idempotentMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'] } - Opt in a single request:
context: new HttpContext().set(MTE_RETRY_UNSAFE, true) - Disable entirely:
autoRetry: false
Per-request controls (HttpContext)
import { MTE_BYPASS, MTE_ENCODE_TYPE, MTE_RETRY_UNSAFE } from 'mte-relay-angular';
// Force strict MTE encoding for one request (default is MKE):
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 auto-retry for one non-idempotent request:
this.http.post(url, body, { context: new HttpContext().set(MTE_RETRY_UNSAFE, true) });Streaming and server-sent events
Native EventSource cannot traverse the relay (relay traffic is binary POST frames). Two supported paths:
1. HttpClient streaming — decoded relay streams (MKE-progressive bodies, SSE passthrough) 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 EventSource replacement with parsed events:
private sse = inject(MteEventSource);
this.sse.connect('https://relay.example.com/sse/counter').subscribe((event) => {
console.log(event.type, event.data, event.id);
});
// Unsubscribe to close the stream (aborts the underlying relay request).Caveats, by design (v1):
- No automatic reconnection and no
Last-Event-IDreplay — resubscribe to reconnect. - Each active stream reserves one encoder/decoder pair for its lifetime. Many concurrent streams can exhaust the pool (
maxPairs, default 15) and fail withMteRelayCapacityError. BudgetmaxPairsaccordingly.
Error handling
All failures surface as standard HttpErrorResponse, so existing error interceptors keep working. For relay-specific failures, the typed engine error rides in .error:
import { MteRelayHttpError, MteRelayCapacityError, isRecoveredRelayError } 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 / .pairReplaced report the recovery already performed
}
...
})| Failure | status | .error |
| --- | --- | --- |
| Relay error 559–569 | the relay code | MteRelayHttpError (repaired/pairReplaced flags) |
| Local MTE protocol failure | 0 | MteRelayProtocolError |
| Pair pool exhausted | 0 | MteRelayCapacityError |
| Upstream non-2xx | upstream status | decoded upstream body (stock behavior) |
Zones, zoneless, SSR
- All engine work (WASM, pairing, keep-alive timers, stream pumps) runs outside the Angular zone — it never blocks
ApplicationRef.isStable(SSR serialization, hydration) and never triggers gratuitous change detection. - Events are delivered back in the zone you subscribed from, so change detection works in zoneful apps; zoneless apps work naturally via observable emissions.
onPoolEventandstreamDiagnosticscallbacks fire outside the zone.- SSR: the MTE engine is browser-only. During server rendering, relay-origin requests either go plaintext via the passthrough backend (
ssr: 'passthrough', default — document this in your threat model) or fail fast (ssr: 'block'). The server-side passthrough is always fetch-based automatically. eagerInit: truewarms up WASM + license at startup viaAPP_INITIALIZER; a warm-up failure logs and falls back to lazy init rather than blocking bootstrap.
Testing your app
Use provideMteRelayTesting() from the secondary entry point — a pure bypass that composes with Angular's standard HTTP testing:
import { provideMteRelayTesting, createMteRelayHttpErrorResponse } from 'mte-relay-angular/testing';
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(), // HttpTestingController works untouched
provideMteRelayTesting(),
],
});The WASM runtime is never loaded in tests (it cannot load under jsdom/Node). To test relay-failure handling, throw factory-built errors from your mocks:
httpSpy.get.and.returnValue(throwError(() =>
createMteRelayHttpErrorResponse(562, { pairReplaced: true })));Known limitations
- No
HttpEventType.UploadProgressfor relayed requests. The engine MTE-encodes the entire request body before transmitting, so no progress exists during the encode phase; transmit-phase progress would be technically possible with an XHR transport but would report encoded (not original) byte counts, and is not currently implemented. This matches Angular 22+'s defaultFetchBackend, which also has no upload progress. Passthrough (non-relay) traffic still gets upload progress on the'xhr'fallback. MteEventSourcev1: no auto-reconnect /Last-Event-ID(see above).- The engine has no teardown API yet — keep-alive timers run for the app's lifetime (they are outside the zone and harmless, but note it for tests/embedded scenarios).
- Server v5 only.
Repository layout
projects/mte-relay-angular/ the library (ng-packagr, APF)
src/lib/
provide-mte-relay.ts providers + ordering assertion + eager init
config.ts config types, validation, origin matching
context-tokens.ts MTE_ENCODE_TYPE / MTE_BYPASS / MTE_RETRY_UNSAFE
engine.service.ts owns the MteRelayClient instance (zone-safe)
backend/
mte-relay-backend.ts HttpBackend: route → relay | passthrough; retry
request-adapter.ts HttpRequest → engine input (HttpXhrBackend parity)
response-adapter.ts engine Response → HttpEvent sequence
passthrough.ts passthrough backend token
sse/ SseParser + MteEventSource
testing/ secondary entry point: bypass provider + error factories
projects/demo/ dev harness app (see below)Developing this library
npm install # needs @eclypses registry access for the mte WASM package
npm run build # ng-packagr build → dist/mte-relay-angular
npm run serve:demo # demo app on http://localhost:4200
npm run pack # build + npm pack tarball for local consumer testingLocal dev currently consumes mte-relay-browser via file:../mte-relay-browser (workspace sibling) because the MteRelayClient export ships in 5.0.0-beta.7, which is not yet published. After it is published, switch the root package.json dependency to the registry version.
To exercise the demo against a real stack: run a local MTE Relay v5 server (../mte-relay, e.g. Dockerfile-mrs) with its upstream pointed at jsonplaceholder (../jsonplaceholder), put a valid license + relay origin in projects/demo/src/main.ts / demo-config.ts, then npm run serve:demo.
The library is pinned to the Angular 17 toolchain on purpose: ng-packagr partial-Ivy output built on the lowest supported major is consumable by every later major. Do not raise the toolchain version without also raising the minimum supported Angular.
Versioning
Semantic versioning. The peer range for @angular/* widens with each Angular major after compatibility verification. See CHANGELOG.md.
