574 lines
19 KiB
TypeScript
574 lines
19 KiB
TypeScript
|
|
/**
|
||
|
|
* Addressing Modes Test
|
||
|
|
*
|
||
|
|
* Tests addressing modes implemented in @open-matrix/federation:
|
||
|
|
* 1. Realm-only addressing (anonymous)
|
||
|
|
* 2. Tab realm addressing (flowpad-demo.tab.xyz)
|
||
|
|
* 3. ~tab URI normalization
|
||
|
|
* 4. Principal-based addressing (anon, jwt, service)
|
||
|
|
* 5. Cross-realm with principal context
|
||
|
|
*
|
||
|
|
* Note: Session directory tests removed (SessionDirectory/SessionsFacade deleted in cleanup)
|
||
|
|
*
|
||
|
|
* Run:
|
||
|
|
* npx tsx --test tests/integration/federation/addressing-modes.spec.ts
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it } from 'node:test';
|
||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import {
|
||
|
|
FederationPeer, ExactInstanceResolver,
|
||
|
|
WIRE_PROFILES,
|
||
|
|
InMemoryFilterBroker,
|
||
|
|
InMemoryExactBroker,
|
||
|
|
getLegacyIngressTopic,
|
||
|
|
getMxIngressTopic,
|
||
|
|
getInternalTopic,
|
||
|
|
createLegacyRequest,
|
||
|
|
createMxEnvV1,
|
||
|
|
serializeLegacyEnvelope,
|
||
|
|
serializeMxEnvV1,
|
||
|
|
parseMatrixUri,
|
||
|
|
CrossRealmClient,
|
||
|
|
} from '@open-matrix/federation';
|
||
|
|
|
||
|
|
function sleep(ms: number): Promise<void> {
|
||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
|
||
|
|
function generateCorrelationId(): string {
|
||
|
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// 1. REALM-ONLY ADDRESSING (Anonymous)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
describe('Realm-Only Addressing', () => {
|
||
|
|
it('anonymous request from realm A to realm B', async () => {
|
||
|
|
const REALM_A = 'client-realm';
|
||
|
|
const REALM_B = 'server-realm';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const localBusA = new InMemoryFilterBroker();
|
||
|
|
const localBusB = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gatewayA = new FederationPeer({
|
||
|
|
localRealm: REALM_A,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus: localBusA,
|
||
|
|
});
|
||
|
|
|
||
|
|
const gatewayB = new FederationPeer({
|
||
|
|
localRealm: REALM_B,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus: localBusB,
|
||
|
|
});
|
||
|
|
|
||
|
|
gatewayA.start();
|
||
|
|
gatewayB.start();
|
||
|
|
|
||
|
|
// Handler on B - receives internal format, replies to replyTo topic
|
||
|
|
localBusB.subscribe(getInternalTopic(REALM_B, 'echo.accepts.ping'), (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload);
|
||
|
|
// Reply to the internal replyTo topic - gateway will forward to backbone
|
||
|
|
localBusB.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { pong: true, from: REALM_B },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// A listens on its backbone mailbox for replies
|
||
|
|
const correlationId = generateCorrelationId();
|
||
|
|
let reply: unknown = null;
|
||
|
|
|
||
|
|
backbone.subscribe(getLegacyIngressTopic(REALM_A), (topic, payload) => {
|
||
|
|
const parsed = JSON.parse(payload);
|
||
|
|
if (parsed.correlationId === correlationId) {
|
||
|
|
reply = parsed;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: REALM_B,
|
||
|
|
toPath: 'echo.accepts.ping',
|
||
|
|
fromRealm: REALM_A,
|
||
|
|
replyTo: '_client.inbox',
|
||
|
|
correlationId,
|
||
|
|
body: { msg: 'hello' },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM_B), serializeLegacyEnvelope(request));
|
||
|
|
|
||
|
|
await sleep(500);
|
||
|
|
|
||
|
|
assert.ok(reply, 'Should receive reply');
|
||
|
|
// Reply body is in 'body' field, not 'result'
|
||
|
|
assert.strictEqual((reply as { body: { pong: boolean } }).body.pong, true);
|
||
|
|
|
||
|
|
console.log(' ✓ Anonymous realm-only addressing works');
|
||
|
|
|
||
|
|
gatewayA.stop();
|
||
|
|
gatewayB.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// 2. TAB REALM ADDRESSING
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
describe('Tab Realm Addressing', () => {
|
||
|
|
it('tab realm format: home.tab.xyz communicates with home realm', async () => {
|
||
|
|
const HOME_REALM = 'flowpad-demo';
|
||
|
|
const TAB_REALM = 'flowpad-demo.tab.abc123';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const homeBus = new InMemoryFilterBroker();
|
||
|
|
const tabBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
// Home realm gateway
|
||
|
|
const homeGateway = new FederationPeer({
|
||
|
|
localRealm: HOME_REALM,
|
||
|
|
profile: WIRE_PROFILES.mxenv1,
|
||
|
|
backbone,
|
||
|
|
localBus: homeBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Tab realm gateway
|
||
|
|
const tabGateway = new FederationPeer({
|
||
|
|
localRealm: TAB_REALM,
|
||
|
|
profile: WIRE_PROFILES.mxenv1,
|
||
|
|
backbone,
|
||
|
|
localBus: tabBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
homeGateway.start();
|
||
|
|
tabGateway.start();
|
||
|
|
|
||
|
|
// Home realm handler - replies to internal replyTo topic (gateway forwards to backbone)
|
||
|
|
homeBus.subscribe(getInternalTopic(HOME_REALM, 'service.accepts.getData'), (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload);
|
||
|
|
// Reply to the internal replyTo topic - gateway handles forwarding
|
||
|
|
homeBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { data: 'from home realm', tabRealm: TAB_REALM },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// Tab listens on its backbone mailbox for replies
|
||
|
|
let tabReceived: unknown = null;
|
||
|
|
backbone.subscribe(getMxIngressTopic(TAB_REALM, '00'), (topic, payload) => {
|
||
|
|
const parsed = JSON.parse(payload);
|
||
|
|
if (parsed.msgType === 'Result') {
|
||
|
|
tabReceived = parsed;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
const correlationId = generateCorrelationId();
|
||
|
|
// NOTE: createMxEnvV1 uses replyToRealm/replyToPath, NOT replyTo object
|
||
|
|
const request = createMxEnvV1({
|
||
|
|
msgType: 'Cmd',
|
||
|
|
toRealm: HOME_REALM,
|
||
|
|
toPath: 'service.accepts.getData',
|
||
|
|
fromRealm: TAB_REALM,
|
||
|
|
fromPath: '_client',
|
||
|
|
replyToRealm: TAB_REALM,
|
||
|
|
replyToPath: '_inbox.emits.reply',
|
||
|
|
correlationId,
|
||
|
|
ttl: 8,
|
||
|
|
body: { query: 'test' },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getMxIngressTopic(HOME_REALM, '00'), serializeMxEnvV1(request));
|
||
|
|
|
||
|
|
await sleep(500);
|
||
|
|
|
||
|
|
assert.ok(tabReceived, 'Tab should receive reply from home');
|
||
|
|
assert.strictEqual((tabReceived as { body: { tabRealm: string } }).body.tabRealm, TAB_REALM);
|
||
|
|
|
||
|
|
console.log(' ✓ Tab realm addressing (home.tab.xyz) works');
|
||
|
|
|
||
|
|
homeGateway.stop();
|
||
|
|
tabGateway.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('multiple tabs under same home realm are isolated', async () => {
|
||
|
|
const HOME_REALM = 'app';
|
||
|
|
const TAB_1 = 'app.tab.tab1';
|
||
|
|
const TAB_2 = 'app.tab.tab2';
|
||
|
|
const TAB_3 = 'app.tab.tab3';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const tab1Bus = new InMemoryFilterBroker();
|
||
|
|
const tab2Bus = new InMemoryFilterBroker();
|
||
|
|
const tab3Bus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const tab1Gw = new FederationPeer({ localRealm: TAB_1, profile: WIRE_PROFILES.mxenv1, backbone, localBus: tab1Bus });
|
||
|
|
const tab2Gw = new FederationPeer({ localRealm: TAB_2, profile: WIRE_PROFILES.mxenv1, backbone, localBus: tab2Bus });
|
||
|
|
const tab3Gw = new FederationPeer({ localRealm: TAB_3, profile: WIRE_PROFILES.mxenv1, backbone, localBus: tab3Bus });
|
||
|
|
|
||
|
|
tab1Gw.start();
|
||
|
|
tab2Gw.start();
|
||
|
|
tab3Gw.start();
|
||
|
|
|
||
|
|
const received: Record<string, number> = { tab1: 0, tab2: 0, tab3: 0 };
|
||
|
|
|
||
|
|
tab1Bus.subscribe(getInternalTopic(TAB_1, 'notify.emits.event'), () => { received.tab1++; });
|
||
|
|
tab2Bus.subscribe(getInternalTopic(TAB_2, 'notify.emits.event'), () => { received.tab2++; });
|
||
|
|
tab3Bus.subscribe(getInternalTopic(TAB_3, 'notify.emits.event'), () => { received.tab3++; });
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// Send event ONLY to tab2
|
||
|
|
const evt = createMxEnvV1({
|
||
|
|
msgType: 'Evt',
|
||
|
|
toRealm: TAB_2,
|
||
|
|
toPath: 'notify.emits.event',
|
||
|
|
fromRealm: 'broadcaster',
|
||
|
|
fromPath: '_sender',
|
||
|
|
ttl: 8,
|
||
|
|
body: { msg: 'only for tab2' },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getMxIngressTopic(TAB_2, '00'), serializeMxEnvV1(evt));
|
||
|
|
|
||
|
|
await sleep(300);
|
||
|
|
|
||
|
|
assert.strictEqual(received.tab1, 0, 'Tab1 should NOT receive');
|
||
|
|
assert.strictEqual(received.tab2, 1, 'Tab2 should receive');
|
||
|
|
assert.strictEqual(received.tab3, 0, 'Tab3 should NOT receive');
|
||
|
|
|
||
|
|
console.log(' ✓ Multiple tabs are isolated - only tab2 received');
|
||
|
|
|
||
|
|
tab1Gw.stop();
|
||
|
|
tab2Gw.stop();
|
||
|
|
tab3Gw.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// 3. ~TAB URI NORMALIZATION
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
describe('~tab URI Normalization', () => {
|
||
|
|
it('parses ~tab in legacy compact form', () => {
|
||
|
|
const uri = 'flowpad-demo/~tab/xyz/root.ui.app.accepts.ping';
|
||
|
|
const parsed = parseMatrixUri(uri);
|
||
|
|
|
||
|
|
assert.ok(parsed, 'Should parse URI');
|
||
|
|
assert.strictEqual(parsed.realm, 'flowpad-demo.tab.xyz', 'Realm should be normalized');
|
||
|
|
assert.strictEqual(parsed.toPath, 'root.ui.app.accepts.ping');
|
||
|
|
|
||
|
|
console.log(' ✓ ~tab normalization in legacy compact form');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('parses ~tab in segmented MXURI/1 form', () => {
|
||
|
|
const uri = 'flowpad-demo/~tab/abc123/-/root/ui/app/accepts/ping';
|
||
|
|
const parsed = parseMatrixUri(uri);
|
||
|
|
|
||
|
|
assert.ok(parsed, 'Should parse URI');
|
||
|
|
assert.strictEqual(parsed.realm, 'flowpad-demo.tab.abc123', 'Realm should be normalized');
|
||
|
|
assert.strictEqual(parsed.componentPath, 'root.ui.app');
|
||
|
|
assert.strictEqual(parsed.plane, 'accepts');
|
||
|
|
assert.strictEqual(parsed.messageName, 'ping');
|
||
|
|
|
||
|
|
console.log(' ✓ ~tab normalization in segmented MXURI/1 form');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles query parameters with ~tab', () => {
|
||
|
|
const uri = 'home/~tab/session-42/service.accepts.query?depth=3&format=json';
|
||
|
|
const parsed = parseMatrixUri(uri);
|
||
|
|
|
||
|
|
assert.ok(parsed, 'Should parse URI');
|
||
|
|
assert.strictEqual(parsed.realm, 'home.tab.session-42');
|
||
|
|
assert.strictEqual(parsed.query.depth, '3');
|
||
|
|
assert.strictEqual(parsed.query.format, 'json');
|
||
|
|
|
||
|
|
console.log(' ✓ ~tab with query parameters');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// 4. PRINCIPAL-BASED ADDRESSING
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
describe('Principal-Based Addressing', () => {
|
||
|
|
it('MXENV/1 envelope with JWT principal', async () => {
|
||
|
|
const REALM_A = 'client';
|
||
|
|
const REALM_B = 'server';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const busA = new InMemoryFilterBroker();
|
||
|
|
const busB = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gwA = new FederationPeer({ localRealm: REALM_A, profile: WIRE_PROFILES.mxenv1, backbone, localBus: busA });
|
||
|
|
const gwB = new FederationPeer({ localRealm: REALM_B, profile: WIRE_PROFILES.mxenv1, backbone, localBus: busB });
|
||
|
|
|
||
|
|
gwA.start();
|
||
|
|
gwB.start();
|
||
|
|
|
||
|
|
// EdgeGateway passes principalKey (string), not full principal object
|
||
|
|
// JWT principal derives to: `${iss}:${sub}` = "https://auth.example.com:user-123"
|
||
|
|
let receivedPrincipalKey: string | undefined = undefined;
|
||
|
|
|
||
|
|
busB.subscribe(getInternalTopic(REALM_B, 'auth.accepts.check'), (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload);
|
||
|
|
receivedPrincipalKey = req._mx?.principalKey;
|
||
|
|
busB.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { authenticated: true },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// Send request with JWT principal
|
||
|
|
// NOTE: createMxEnvV1 doesn't support principal directly - we add it manually
|
||
|
|
const request = createMxEnvV1({
|
||
|
|
msgType: 'Cmd',
|
||
|
|
toRealm: REALM_B,
|
||
|
|
toPath: 'auth.accepts.check',
|
||
|
|
fromRealm: REALM_A,
|
||
|
|
fromPath: '_client',
|
||
|
|
replyToRealm: REALM_A,
|
||
|
|
replyToPath: '_inbox',
|
||
|
|
correlationId: generateCorrelationId(),
|
||
|
|
ttl: 8,
|
||
|
|
body: { action: 'verify' },
|
||
|
|
});
|
||
|
|
// Add principal manually (createMxEnvV1 doesn't support it)
|
||
|
|
request.principal = {
|
||
|
|
kind: 'jwt',
|
||
|
|
iss: 'https://auth.example.com',
|
||
|
|
sub: 'user-123',
|
||
|
|
aud: ['server'],
|
||
|
|
scopes: ['read', 'write'],
|
||
|
|
};
|
||
|
|
|
||
|
|
backbone.publish(getMxIngressTopic(REALM_B, '00'), serializeMxEnvV1(request));
|
||
|
|
|
||
|
|
await sleep(300);
|
||
|
|
|
||
|
|
// Check principalKey is derived correctly: iss:sub = "https://auth.example.com:user-123"
|
||
|
|
assert.ok(receivedPrincipalKey, 'Handler should receive principalKey');
|
||
|
|
assert.strictEqual(receivedPrincipalKey, 'https://auth.example.com:user-123');
|
||
|
|
|
||
|
|
console.log(' ✓ JWT principal in MXENV/1 envelope → principalKey: ' + receivedPrincipalKey);
|
||
|
|
|
||
|
|
gwA.stop();
|
||
|
|
gwB.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('MXENV/1 envelope with service principal', async () => {
|
||
|
|
const REALM = 'api-server';
|
||
|
|
const FROM_REALM = 'scheduler-service';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gw = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus });
|
||
|
|
gw.start();
|
||
|
|
|
||
|
|
// Service principal derives to: `service:${sub}` = "service:cron-scheduler"
|
||
|
|
let receivedPrincipalKey: string | undefined = undefined;
|
||
|
|
|
||
|
|
localBus.subscribe(getInternalTopic(REALM, 'internal.accepts.sync'), (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload);
|
||
|
|
receivedPrincipalKey = req._mx?.principalKey;
|
||
|
|
// Reply so the gateway doesn't timeout
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { synced: true },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// Send with service principal - MUST include replyTo and correlationId for Cmd messages
|
||
|
|
const request = createMxEnvV1({
|
||
|
|
msgType: 'Cmd',
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'internal.accepts.sync',
|
||
|
|
fromRealm: FROM_REALM,
|
||
|
|
fromPath: '_scheduler',
|
||
|
|
replyToRealm: FROM_REALM,
|
||
|
|
replyToPath: '_inbox',
|
||
|
|
correlationId: generateCorrelationId(),
|
||
|
|
ttl: 8,
|
||
|
|
body: { task: 'cleanup' },
|
||
|
|
});
|
||
|
|
// Add principal manually
|
||
|
|
request.principal = {
|
||
|
|
kind: 'service',
|
||
|
|
sub: 'cron-scheduler',
|
||
|
|
scopes: ['internal:sync'],
|
||
|
|
};
|
||
|
|
|
||
|
|
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(request));
|
||
|
|
|
||
|
|
await sleep(300);
|
||
|
|
|
||
|
|
// Check principalKey is derived correctly: service:sub = "service:cron-scheduler"
|
||
|
|
assert.ok(receivedPrincipalKey, 'Handler should receive principalKey');
|
||
|
|
assert.strictEqual(receivedPrincipalKey, 'service:cron-scheduler');
|
||
|
|
|
||
|
|
console.log(' ✓ Service principal in MXENV/1 envelope → principalKey: ' + receivedPrincipalKey);
|
||
|
|
|
||
|
|
gw.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('anonymous principal when none specified', async () => {
|
||
|
|
const REALM = 'public-api';
|
||
|
|
const FROM_REALM = 'anonymous-client';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gw = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus });
|
||
|
|
gw.start();
|
||
|
|
|
||
|
|
// Anonymous principal derives to: `anon:${fromRealm}` = "anon:anonymous-client"
|
||
|
|
let receivedPrincipalKey: string | undefined = undefined;
|
||
|
|
|
||
|
|
localBus.subscribe(getInternalTopic(REALM, 'public.accepts.query'), (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload);
|
||
|
|
receivedPrincipalKey = req._mx?.principalKey;
|
||
|
|
// Reply so gateway doesn't timeout
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { found: true },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// Send without principal (anonymous)
|
||
|
|
const request = createMxEnvV1({
|
||
|
|
msgType: 'Cmd',
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'public.accepts.query',
|
||
|
|
fromRealm: FROM_REALM,
|
||
|
|
fromPath: '_anon',
|
||
|
|
// NO principal field - will derive to anon
|
||
|
|
replyToRealm: FROM_REALM,
|
||
|
|
replyToPath: '_inbox',
|
||
|
|
correlationId: generateCorrelationId(),
|
||
|
|
ttl: 8,
|
||
|
|
body: { q: 'search' },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(request));
|
||
|
|
|
||
|
|
await sleep(300);
|
||
|
|
|
||
|
|
// Check principalKey is derived correctly: anon:fromRealm = "anon:anonymous-client"
|
||
|
|
assert.ok(receivedPrincipalKey, 'Handler should receive principalKey');
|
||
|
|
assert.strictEqual(receivedPrincipalKey, 'anon:anonymous-client');
|
||
|
|
|
||
|
|
console.log(' ✓ Anonymous principal → principalKey: ' + receivedPrincipalKey);
|
||
|
|
|
||
|
|
gw.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// 5. CROSS-REALM CLIENT WITH DISCOVERY
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
describe('Cross-Realm Client', () => {
|
||
|
|
it('invoke with automatic reply handling', async () => {
|
||
|
|
const SERVER_REALM = 'calculator-service';
|
||
|
|
const CLIENT_REALM = 'client-app';
|
||
|
|
|
||
|
|
const backbone = new InMemoryExactBroker();
|
||
|
|
const serverBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const serverGw = new FederationPeer({
|
||
|
|
localRealm: SERVER_REALM,
|
||
|
|
profile: WIRE_PROFILES.mxenv1,
|
||
|
|
backbone,
|
||
|
|
localBus: serverBus,
|
||
|
|
});
|
||
|
|
serverGw.start();
|
||
|
|
|
||
|
|
// Server handler
|
||
|
|
serverBus.subscribe(getInternalTopic(SERVER_REALM, 'math.accepts.add'), (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload);
|
||
|
|
const { a, b } = req.params;
|
||
|
|
const result = a + b;
|
||
|
|
|
||
|
|
// Reply
|
||
|
|
const reply = createMxEnvV1({
|
||
|
|
msgType: 'Result',
|
||
|
|
toRealm: req._mx?.replyTo?.realm ?? CLIENT_REALM,
|
||
|
|
toPath: req._mx?.replyTo?.path ?? '_inbox',
|
||
|
|
fromRealm: SERVER_REALM,
|
||
|
|
fromPath: 'math',
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
body: { sum: result },
|
||
|
|
});
|
||
|
|
backbone.publish(getMxIngressTopic(req._mx?.replyTo?.realm ?? CLIENT_REALM, '00'), serializeMxEnvV1(reply));
|
||
|
|
});
|
||
|
|
|
||
|
|
await sleep(100);
|
||
|
|
|
||
|
|
// Client uses CrossRealmClient
|
||
|
|
const client = new CrossRealmClient({
|
||
|
|
localRealm: CLIENT_REALM,
|
||
|
|
transport: backbone,
|
||
|
|
profile: WIRE_PROFILES.mxenv1,
|
||
|
|
defaultTimeoutMs: 5000,
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await client.invoke<{ sum: number }>(SERVER_REALM, 'math.accepts.add', { a: 15, b: 27 });
|
||
|
|
|
||
|
|
assert.strictEqual(result.sum, 42);
|
||
|
|
console.log(' ✓ CrossRealmClient invoke: 15 + 27 = 42');
|
||
|
|
|
||
|
|
serverGw.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// SUMMARY
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
describe('Addressing Modes Summary', () => {
|
||
|
|
it('documents all tested addressing modes', () => {
|
||
|
|
const modes = [
|
||
|
|
{ name: 'Realm-only (anonymous)', tested: true },
|
||
|
|
{ name: 'Tab realm (home.tab.xyz)', tested: true },
|
||
|
|
{ name: '~tab URI normalization', tested: true },
|
||
|
|
{ name: 'JWT principal', tested: true },
|
||
|
|
{ name: 'Service principal', tested: true },
|
||
|
|
{ name: 'Anonymous principal', tested: true },
|
||
|
|
{ name: 'CrossRealmClient invoke', tested: true },
|
||
|
|
];
|
||
|
|
|
||
|
|
console.log('\n ┌────────────────────────────────────────────┐');
|
||
|
|
console.log(' │ ADDRESSING MODES COVERAGE │');
|
||
|
|
console.log(' ├────────────────────────────────────────────┤');
|
||
|
|
for (const m of modes) {
|
||
|
|
console.log(` │ ${m.tested ? '✓' : '✗'} ${m.name.padEnd(38)} │`);
|
||
|
|
}
|
||
|
|
console.log(' └────────────────────────────────────────────┘');
|
||
|
|
|
||
|
|
assert.ok(modes.every(m => m.tested), 'All addressing modes should be tested');
|
||
|
|
});
|
||
|
|
});
|