blob: 8f6ce7675f9c5304151670feed5cfd07fa4f496f [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { waitFor } from '@maka/core/test-only/async-primitives';
import assert from 'node:assert/strict';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, relative } from 'node:path';
import { setImmediate as waitForImmediate } from 'node:timers/promises';
import { test } from 'node:test';
import {
createRuntimeHostPeerClient,
RuntimeHostPeerReachabilityUnavailableError,
type RuntimeHostPeerRouteResolution,
} from '../client/peer-client.js';
import { PEER_REACHABILITY_MAX_CLOCK_SKEW_MS } from '../peer-reachability/index.js';
import {
ensureRuntimeHostPeerIdentity,
normalizePeerError,
readRuntimeHostPeerAuthentication,
readRuntimeHostPeerAuthenticationResult,
RuntimeHostPeerError,
startRuntimeHostPeerEndpoint,
type RuntimeHostPeerNativeStream,
} from '../transport/peer-native.js';
test('preserves transit route failures from the native boundary', () => {
const error = normalizePeerError(new Error('transit_unavailable: no approved route'));
assert.equal(error.code, 'transit_unavailable');
assert.equal(error.message, 'no approved route');
});
test('shares one endpoint with independent application and Mesh dial lanes', async () => {
const directory = await mkdtemp(join(tmpdir(), 'maka-peer-abort-'));
const nativePath = join(directory, 'peer.cjs');
try {
await writeFile(
nativePath,
`let finishAccept;
let finishMeshAccept;
let finishConnectivity;
let connectivity = { generation: 0, connectedPeerIds: [] };
const pending = new Map();
const stats = { starts: 0, closes: 0, requests: [], updates: [], cancellations: [] };
let missFirstCancellation = true;
const stream = { read: async () => null, write: async () => {}, close: async () => {}, abort: () => {} };
module.exports = {
stats,
resolveConnect: (requestId) => {
pending.get(requestId)?.resolve(stream);
pending.delete(requestId);
},
establishPeer: (peerId) => {
connectivity = { generation: connectivity.generation + 1, connectedPeerIds: [peerId] };
finishConnectivity?.(connectivity);
finishConnectivity = undefined;
},
failEndpoint: () => { finishAccept?.(null); finishMeshAccept?.(null); },
ensurePeerIdentity: async () => 'client',
signPeerIdentity: async () => ({ publicKey: Buffer.from('public'), signature: Buffer.from('signature') }),
verifyPeerIdentity: () => true,
startPeerEndpoint: () => {
stats.starts += 1;
return {
peerId: 'client',
reachabilitySnapshot: { generation: 0, listenAddresses: [], activeCoordinationRelays: [] },
get connectivitySnapshot() { return connectivity; },
transitSnapshot: { allowedPeerCount: 0, activeReservationCount: 0, activeCircuitCount: 0, maxReservationCount: 32, maxCircuitCount: 8, maxCircuitsPerPeer: 2, maxCircuitDurationSeconds: 7_200, maxCircuitBytes: 256 * 1024 * 1024 },
watchReachability: async () => 0,
watchConnectivity: async (afterGeneration) => connectivity.generation === afterGeneration
? new Promise((resolve) => { finishConnectivity = resolve; })
: connectivity,
connect: ({ requestId, peerId, routeHints, coordinationRelays, transitRelayPeerIds }) => {
stats.requests.push({ requestId, peerId, routeHints, coordinationRelays, transitRelayPeerIds });
if (peerId === 'unreachable') return Promise.reject(Object.assign(new Error('transit_unavailable: no approved route'), { code: 'GenericFailure' }));
if (peerId === 'ready' || peerId === 'fallback' || peerId === 'observed') return Promise.resolve(stream);
return new Promise((resolve, reject) => pending.set(requestId, { peerId, resolve, reject }));
},
connectMeshControl: ({ requestId, peerId, routeHints, coordinationRelays, transitRelayPeerIds }) => {
stats.requests.push({ requestId, peerId, routeHints, coordinationRelays, transitRelayPeerIds });
if (peerId === 'ready' || peerId === 'self-contained') return Promise.resolve(stream);
return new Promise((resolve, reject) => pending.set(requestId, { peerId, resolve, reject }));
},
configureTransit: async () => {},
updateConnect: async (options) => {
stats.updates.push(options);
const request = pending.get(options.requestId);
if (request?.peerId === 'self-contained') {
request.resolve(stream);
pending.delete(options.requestId);
}
return Boolean(request);
},
cancelConnect: async (requestId) => {
stats.cancellations.push(requestId);
if (missFirstCancellation) {
missFirstCancellation = false;
return false;
}
pending.get(requestId)?.reject(new Error('peer_connect_cancelled: cancelled'));
pending.delete(requestId);
return true;
},
accept: () => new Promise((resolve) => { finishAccept = resolve; }),
acceptMeshControl: () => new Promise((resolve) => { finishMeshAccept = resolve; }),
close: async () => { stats.closes += 1; finishAccept?.(null); finishMeshAccept?.(null); finishConnectivity?.(connectivity); },
};
},
};
`,
);
let routesPrepared = false;
let selfContainedRoutesPrepared = false;
let recoveryExhausted = false;
const preparedPeerIds: string[] = [];
const client = createRuntimeHostPeerClient({
nativePath,
keyPath: join(directory, 'peer.key'),
});
const detachRouteResolver = client.attachRouteResolver({
prepareRoutes: async (peerId) => {
preparedPeerIds.push(peerId);
if (peerId === 'exhausted') {
recoveryExhausted = true;
return;
}
routesPrepared = true;
if (peerId === 'fallback') throw new Error('Mesh refresh failed');
if (peerId === 'self-contained') {
await new Promise((resolve) => setImmediate(resolve));
selfContainedRoutesPrepared = true;
}
},
resolveRoutes: (peerId) =>
peerId === 'exhausted'
? {
state: recoveryExhausted ? 'exhausted' : 'recovering',
routeHints: [],
coordinationRelays: [],
transitRelayPeerIds: [],
}
: peerId !== 'observed' &&
(peerId === 'self-contained' ? selfContainedRoutesPrepared : routesPrepared)
? {
state: 'available',
routeHints: ['/memory/discovered'],
coordinationRelays: ['/memory/relay'],
transitRelayPeerIds: ['transit-peer'],
}
: {
state: 'recovering',
routeHints: [],
coordinationRelays: [],
transitRelayPeerIds: [],
},
subscribeRoutes: () => () => undefined,
});
const native = await import(nativePath);
const phases: string[] = [];
const abort = new AbortController();
const pending = client.connect(peerConnectInput('pending'), abort.signal, (phase) => {
phases.push(phase);
});
await waitForRequestCount(native.default.stats, 1);
assert.equal(routesPrepared, true);
assert.deepEqual(phases, ['discovering', 'connecting']);
abort.abort();
await assert.rejects(pending, /aborted/u);
const application = client.connect(peerConnectInput('shared'));
await waitForRequestCount(native.default.stats, 2);
const queuedAbort = new AbortController();
const cancelled = client.connect(peerConnectInput('shared'), queuedAbort.signal);
queuedAbort.abort();
await assert.rejects(cancelled, /aborted/u);
const control = client.connectMeshControl(peerConnectInput('shared'));
await waitForRequestCount(native.default.stats, 3);
assert.equal(native.default.stats.requests.length, 3);
native.default.resolveConnect(3);
await control;
native.default.resolveConnect(2);
await application;
const preparedBeforeReopen = preparedPeerIds.length;
const reopenPhases: string[] = [];
await client.connect(
{ ...peerConnectInput('ready'), refreshRoutes: false },
undefined,
(phase) => reopenPhases.push(phase),
);
assert.equal(preparedPeerIds.length, preparedBeforeReopen);
assert.deepEqual(reopenPhases, ['connecting']);
await client.connect(peerConnectInput('fallback'));
await assert.rejects(client.connect(peerConnectInput('unreachable')), (failure: unknown) => {
return failure instanceof RuntimeHostPeerError && failure.code === 'transit_unavailable';
});
const selfContained = client.connect({
...peerConnectInput('self-contained'),
coordinationRelays: ['/memory/explicit-relay'],
});
await selfContained;
assert.equal(preparedPeerIds.includes('self-contained'), true);
client.observeAuthenticatedReachability({
expectedPeerId: 'observed',
value: signedReachability('observed', ['/memory/fresh'], ['/memory/fresh-relay']),
});
await client.connect({
...peerConnectInput('observed'),
routeHints: ['/memory/stale'],
coordinationRelays: ['/memory/stale-relay'],
});
assert.equal(native.default.stats.starts, 1);
assert.equal(native.default.stats.closes, 0);
assert.equal(native.default.stats.requests.length, 8);
assert.equal(
native.default.stats.requests.filter(
({ peerId }: { peerId: string }) => peerId === 'self-contained',
).length,
1,
);
assert.deepEqual(native.default.stats.requests[0], {
requestId: 1,
peerId: 'pending',
routeHints: ['/memory/1'],
coordinationRelays: [],
transitRelayPeerIds: [],
});
assert.deepEqual(native.default.stats.updates[0], {
requestId: 1,
routeHints: ['/memory/discovered', '/memory/1'],
coordinationRelays: ['/memory/relay'],
transitRelayPeerIds: ['transit-peer'],
});
assert.deepEqual(native.default.stats.requests.at(-1), {
requestId: 8,
peerId: 'observed',
routeHints: ['/memory/fresh', '/memory/stale'],
coordinationRelays: ['/memory/fresh-relay', '/memory/stale-relay'],
transitRelayPeerIds: [],
});
for (let index = 0; index < 160; index += 1) {
const peerId = `remembered-${index}`;
client.observeAuthenticatedReachability({
expectedPeerId: peerId,
value: signedReachability(peerId, [`/memory/${peerId}`], []),
});
}
await client.connect({
...peerConnectInput('observed'),
routeHints: ['/memory/stale'],
coordinationRelays: ['/memory/stale-relay'],
refreshRoutes: false,
});
assert.deepEqual(native.default.stats.requests.at(-1), {
requestId: 9,
peerId: 'observed',
routeHints: ['/memory/stale'],
coordinationRelays: ['/memory/stale-relay'],
transitRelayPeerIds: [],
});
assert.deepEqual(native.default.stats.cancellations, [1, 1]);
const rolledBackIssuedAt = Date.now() + PEER_REACHABILITY_MAX_CLOCK_SKEW_MS + 1;
client.observeAuthenticatedReachability({
expectedPeerId: 'ready',
value: signedReachability('ready', ['/memory/before-clock-reset'], [], rolledBackIssuedAt),
allowHistorical: true,
});
await client.connect({
...peerConnectInput('ready'),
routeHints: [],
refreshRoutes: false,
});
const historicalRequest = native.default.stats.requests.at(-1) as {
readonly peerId: string;
readonly routeHints: readonly string[];
};
assert.equal(historicalRequest.peerId, 'ready');
assert.equal(historicalRequest.routeHints.includes('/memory/before-clock-reset'), true);
await assert.rejects(
client.connect({
...peerConnectInput('exhausted'),
routeHints: [],
}),
(error: unknown) =>
error instanceof RuntimeHostPeerReachabilityUnavailableError &&
error.code === 'peer_reachability_needs_repair',
);
detachRouteResolver();
const requestCount = native.default.stats.requests.length;
await assert.rejects(
client.connect({
...peerConnectInput('needs-repair'),
routeHints: [],
refreshRoutes: false,
}),
(error: unknown) => error instanceof RuntimeHostPeerReachabilityUnavailableError,
);
assert.equal(native.default.stats.requests.length, requestCount);
let resolution: RuntimeHostPeerRouteResolution = {
state: 'exhausted',
routeHints: [],
coordinationRelays: [],
transitRelayPeerIds: [],
};
let notifyResolution = () => {};
const detachExhausted = client.attachRouteResolver({
resolveRoutes: () => resolution,
subscribeRoutes: (_peerId, listener) => {
notifyResolution = listener;
return () => {};
},
prepareRoutes: async () => {
resolution = { ...resolution, state: 'recovering' };
notifyResolution();
resolution = { ...resolution, state: 'exhausted' };
notifyResolution();
},
});
let recoveryWakeups = 0;
const unsubscribeRecovery = client.subscribeRoutes('offline', () => recoveryWakeups++);
for (let attempt = 0; attempt < 3; attempt++) {
await assert.rejects(
client.connect({ ...peerConnectInput('offline'), routeHints: [] }),
RuntimeHostPeerReachabilityUnavailableError,
);
}
assert.equal(recoveryWakeups, 0, 'empty recovery sweeps must not wake their own retries');
resolution = { ...resolution, state: 'available', coordinationRelays: ['/memory/new-relay'] };
notifyResolution();
assert.equal(recoveryWakeups, 1, 'a new candidate wakes the offline connection');
notifyResolution();
assert.equal(recoveryWakeups, 1, 'unchanged candidates do not bypass backoff');
resolution = { ...resolution, state: 'exhausted', coordinationRelays: [] };
notifyResolution();
assert.equal(recoveryWakeups, 1, 'losing the final candidate does not wake a retry');
unsubscribeRecovery();
detachExhausted();
let connectivityWakeups = 0;
const unsubscribeConnectivity = client.subscribeRoutes('restored', () => {
connectivityWakeups += 1;
});
native.default.establishPeer('restored');
await waitForImmediate();
assert.equal(connectivityWakeups, 1);
unsubscribeConnectivity();
const connectedPeers: string[] = [];
const detachRecovery = client.attachRouteResolver({
prepareRoutes: async () => {},
resolveRoutes: () => ({
state: 'exhausted',
routeHints: [],
coordinationRelays: [],
transitRelayPeerIds: [],
}),
subscribeRoutes: () => () => {},
peerConnected: (peerId) => {
connectedPeers.push(peerId);
if (peerId === 'restored') throw new Error('recovery observer failed');
},
});
assert.deepEqual(connectedPeers, ['restored'], 'attachment observes an already connected peer');
native.default.establishPeer('ready');
await waitForImmediate();
assert.deepEqual(connectedPeers, ['restored', 'ready']);
native.default.establishPeer('ready');
await waitForImmediate();
assert.deepEqual(
connectedPeers,
['restored', 'ready'],
'snapshot refreshes are not new connections',
);
detachRecovery();
native.default.establishPeer('detached');
await waitForImmediate();
assert.deepEqual(connectedPeers, ['restored', 'ready']);
native.default.failEndpoint();
await waitForImmediate();
await assert.rejects(
client.connect(peerConnectInput('ready')),
/cannot recover until this Client restarts/u,
);
assert.equal(native.default.stats.starts, 1);
await client.close();
assert.equal(native.default.stats.closes, 1);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test('rejects an incomplete endpoint API and loads a compatible relative native module', async () => {
const directory = await mkdtemp(join(tmpdir(), 'maka-peer-native-'));
try {
const incompletePath = join(directory, 'incomplete.cjs');
await writeFile(
incompletePath,
'module.exports = { ensurePeerIdentity: async () => "peer", signPeerIdentity: async () => ({ publicKey: Buffer.from("public"), signature: Buffer.from("signature") }), verifyPeerIdentity: () => true, startPeerEndpoint: () => ({ peerId: "peer", listenAddresses: [] }) };\n',
);
assert.throws(
() =>
startRuntimeHostPeerEndpoint({
nativePath: relative(process.cwd(), incompletePath),
keyPath: 'unused',
}),
(error: unknown) =>
error instanceof RuntimeHostPeerError && error.code === 'peer_native_unavailable',
);
const modulePath = join(directory, 'peer.cjs');
await writeFile(
modulePath,
`const stream = { read: async () => null, write: async () => {}, close: async () => {}, abort: () => {} };
const starts = [];
module.exports = {
starts,
ensurePeerIdentity: async () => 'peer',
signPeerIdentity: async () => ({ publicKey: Buffer.from('public'), signature: Buffer.from('signature') }),
verifyPeerIdentity: () => true,
startPeerEndpoint: (options) => {
starts.push(options);
return ({
peerId: 'peer',
reachabilitySnapshot: { generation: 0, listenAddresses: [], activeCoordinationRelays: [] },
connectivitySnapshot: { generation: 0, connectedPeerIds: [] },
transitSnapshot: { allowedPeerCount: 0, activeReservationCount: 0, activeCircuitCount: 0, maxReservationCount: 32, maxCircuitCount: 8, maxCircuitsPerPeer: 2, maxCircuitDurationSeconds: 7_200, maxCircuitBytes: 256 * 1024 * 1024 },
watchReachability: async () => 0,
watchConnectivity: async () => ({ generation: 0, connectedPeerIds: [] }),
connect: async () => stream,
connectMeshControl: async () => stream,
configureTransit: async () => {},
updateConnect: async () => true,
cancelConnect: async () => true,
accept: async () => null,
acceptMeshControl: async () => null,
close: async () => {},
});
},
};
`,
);
const endpoint = startRuntimeHostPeerEndpoint({
nativePath: relative(process.cwd(), modulePath),
keyPath: 'unused',
webRtcStunUrls: [],
});
assert.equal(endpoint.peerId, 'peer');
const native = await import(modulePath);
assert.deepEqual(native.default.starts, [{ keyPath: 'unused', webRtcStunUrls: [] }]);
assert.equal(
await ensureRuntimeHostPeerIdentity({
nativePath: modulePath,
keyPath: 'unused',
}),
'peer',
);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test('bounds and separates the peer credential preface from Runtime Host frames', async () => {
const frame = Buffer.from('{"kind":"hello"}\n');
const authenticated = await readRuntimeHostPeerAuthentication(
streamWith(Buffer.concat([Buffer.from('{"v":1,"credential":"token"}\n'), frame])),
);
assert.equal(authenticated.credential, 'token');
assert.deepEqual(authenticated.remainder, frame);
await assert.rejects(
readRuntimeHostPeerAuthentication(
streamWith(Buffer.concat([Buffer.alloc(12 * 1024 + 1), Buffer.from('\n')])),
),
(error: unknown) =>
error instanceof RuntimeHostPeerError && /preface is too large/u.test(error.message),
);
const result = await readRuntimeHostPeerAuthenticationResult(
streamWith(Buffer.concat([Buffer.from('{"v":1,"accepted":true}\n'), frame])),
);
assert.equal(result.accepted, true);
assert.deepEqual(result.remainder, frame);
const resume = { sessionId: 'a'.repeat(64), generation: 2, received: 65_536 };
const resumed = await readRuntimeHostPeerAuthentication(
streamWith(
Buffer.concat([
Buffer.from(`${JSON.stringify({ v: 2, credential: 'token', resume })}\n`),
frame,
]),
),
);
assert.deepEqual(resumed.resume, resume);
assert.deepEqual(resumed.remainder, frame);
const resumedResult = await readRuntimeHostPeerAuthenticationResult(
streamWith(Buffer.from('{"v":2,"accepted":true,"resume":{"received":65536}}\n')),
);
assert.equal(resumedResult.resume?.received, 65_536);
await assert.rejects(
readRuntimeHostPeerAuthenticationResult(
streamWith(Buffer.from('{"v":2,"accepted":false,"reason":"capacity_exceeded"}\n')),
),
(error: unknown) =>
error instanceof RuntimeHostPeerError && error.code === 'peer_capacity_exceeded',
);
for (const invalid of [
{ v: 2, accepted: true, reason: 'capacity_exceeded' },
{ v: 2, accepted: false, reason: 'unknown' },
{ v: 2, accepted: false, reason: 'capacity_exceeded', resume: { received: 0 } },
]) {
await assert.rejects(
readRuntimeHostPeerAuthenticationResult(
streamWith(Buffer.from(`${JSON.stringify(invalid)}\n`)),
),
/result is invalid/u,
);
}
for (const invalid of [
{ ...resume, generation: 0 },
{ ...resume, received: -1 },
{ ...resume, sessionId: 'short' },
{ ...resume, extra: true },
]) {
await assert.rejects(
readRuntimeHostPeerAuthentication(
streamWith(
Buffer.from(`${JSON.stringify({ v: 2, credential: 'token', resume: invalid })}\n`),
),
),
/preface is invalid/u,
);
}
});
async function waitForRequestCount(
stats: { readonly requests: readonly unknown[] },
expected: number,
): Promise<void> {
await waitFor(() => stats.requests.length >= expected, {
timeoutMs: 5_000,
pollMs: 10,
message: `peer-native request count did not reach ${expected}`,
});
assert.equal(stats.requests.length, expected);
}
function streamWith(chunk: Buffer): RuntimeHostPeerNativeStream {
let pending: Buffer | null = chunk;
return {
peerId: 'remote-peer',
read: async () => {
const value = pending;
pending = null;
return value;
},
write: async () => undefined,
close: async () => undefined,
abort: () => undefined,
};
}
function signedReachability(
peerId: string,
directRoutes: readonly string[],
coordinationRoutes: readonly string[],
issuedAt = Date.now(),
) {
return {
lease: {
version: 1 as const,
peerId,
revision: 1,
issuedAt,
expiresAt: issuedAt + 60_000,
directRoutes,
coordinationRoutes,
},
publicKey: Buffer.from('public').toString('base64url'),
signature: Buffer.from('signature').toString('base64url'),
};
}
function peerConnectInput(peerId: string) {
return {
peerId,
routeHints: ['/memory/1'],
coordinationRelays: [],
directDeadlineMs: 1_000,
} as const;
}