Compare commits

..
Author SHA1 Message Date
Hampus 1054dacd62 fix(desktop): follow an alias domain to the instance it declares 2026-10-18 21:02:47 +02:00
2 changed files with 75 additions and 4 deletions

No files matched your search

@@ -20,6 +20,7 @@ import {HttpStatus, MimeType} from '@fluxer/constants/src/HttpConstants';
import {DESKTOP_RUNTIME_DISCOVERY_UNREACHABLE_ERROR_NAME} from '@fluxer/desktop_ipc/src/LocalAppRuntimeContract';
import {
buildInstanceDiscoveryCandidates,
InstanceDiscoveryOriginMismatchError,
parseInstanceDiscoveryDocument,
requireDiscoveryDeclaresItsOwnOrigin,
} from '@fluxer/instance_bootstrap/src/Discovery';
@@ -79,6 +80,15 @@ class DesktopRuntimeDiscoveryFailedError extends Error {
}
}
class DesktopRuntimeDiscoveryDeclaredElsewhereError extends DesktopRuntimeDiscoveryFailedError {
public readonly declaredApiEndpoint: string;
public constructor(apiEndpoint: string, attempts: ReadonlyArray<DiscoveryAttempt>, declaredApiEndpoint: string) {
super(apiEndpoint, attempts);
this.declaredApiEndpoint = declaredApiEndpoint;
}
}
class DesktopRuntimeDiscoveryUnreachableError extends DesktopRuntimeDiscoveryFailedError {
public constructor(apiEndpoint: string, attempts: ReadonlyArray<DiscoveryAttempt>) {
super(apiEndpoint, attempts);
@@ -98,9 +108,28 @@ export async function resolveDesktopRuntimePlan({
input,
signal,
}: ResolveDesktopRuntimeConfigInput): Promise<LocalAppRuntimePlan> {
try {
return await resolveDesktopRuntimePlanAt(input, signal);
} catch (error) {
if (!(error instanceof DesktopRuntimeDiscoveryDeclaredElsewhereError)) {
throw error;
}
log.info('Instance discovery declared another API origin, resolving that instance', {
input,
declaredApiEndpoint: error.declaredApiEndpoint,
});
return resolveDesktopRuntimePlanAt(error.declaredApiEndpoint, signal, true);
}
}
async function resolveDesktopRuntimePlanAt(
input: string,
signal: AbortSignal | null,
followed = false,
): Promise<LocalAppRuntimePlan> {
const apiEndpoint = requireDesktopRuntimeAPIEndpoint(input);
const anchorOrigin = requireDesktopHTTPOrigin(new URL(apiEndpoint).origin);
const resolved = await fetchRuntimePlan(apiEndpoint, anchorOrigin, signal);
const resolved = await fetchRuntimePlan(apiEndpoint, anchorOrigin, signal, followed);
let anchorRequirement: DesktopAddressRequirement;
try {
anchorRequirement = await getDesktopOutboundHTTP().registerAnchoredOrigins({
@@ -147,9 +176,11 @@ async function fetchRuntimePlan(
apiEndpoint: string,
anchorOrigin: string,
signal: AbortSignal | null,
followed: boolean,
): Promise<ResolvedRuntimePlan> {
const candidates = buildInstanceDiscoveryCandidates({input: apiEndpoint});
const attempts: Array<DiscoveryAttempt> = [];
let declaredApiEndpoint: string | null = null;
let instanceUnreachable = candidates.length > 0;
for (const url of candidates) {
let document: unknown;
@@ -165,10 +196,16 @@ async function fetchRuntimePlan(
return {plan, served: {url, document}, rememberedAnchorRequirement: null, attempts};
} catch (error) {
instanceUnreachable = false;
if (error instanceof InstanceDiscoveryOriginMismatchError) {
declaredApiEndpoint ??= error.declared;
}
attempts.push({url, reason: error instanceof Error ? error.message : String(error)});
}
}
if (!instanceUnreachable) {
if (declaredApiEndpoint !== null && !followed) {
throw new DesktopRuntimeDiscoveryDeclaredElsewhereError(apiEndpoint, attempts, declaredApiEndpoint);
}
throw new DesktopRuntimeDiscoveryFailedError(apiEndpoint, attempts);
}
const lastServed = await readLastServedDiscovery(apiEndpoint);
@@ -103,11 +103,45 @@ describe('desktop discovery for an account stored on the migrated official domai
});
}
test('another instance that serves a foreign API is still refused', async () => {
network.served.set('https://chat.example.test', officialDocument('https://web.fluxer.app'));
test('an alias domain that declares another API resolves the instance it declares', async () => {
network.served.set('https://domain2.example.test', officialDocument('https://domain1.example.test'));
network.served.set('https://domain1.example.test', officialDocument('https://domain1.example.test'));
await assert.rejects(resolveDesktopRuntimePlan({input: 'https://chat.example.test/api', signal: null}), {
const plan = await resolveDesktopRuntimePlan({input: 'https://domain2.example.test/api', signal: null});
assert.equal(plan.instanceKey, 'https://domain1.example.test/api');
assert.equal(plan.endpoints.apiEndpoint, 'https://domain1.example.test/api');
assert.equal(network.anchored.length, 1);
assert.equal(network.anchored[0].anchorOrigin, 'https://domain1.example.test');
assert.deepEqual(
network.fetched
.filter((request) => new URL(request.url).origin === 'https://domain1.example.test')
.map((request) => request.expectedOrigin),
['https://domain1.example.test'],
);
});
test('the declared instance must declare itself, so a chain stops after one hop', async () => {
network.served.set('https://a.example.test', officialDocument('https://b.example.test'));
network.served.set('https://b.example.test', officialDocument('https://c.example.test'));
network.served.set('https://c.example.test', officialDocument('https://c.example.test'));
await assert.rejects(resolveDesktopRuntimePlan({input: 'https://a.example.test/api', signal: null}), {
name: 'DesktopRuntimeDiscoveryFailedError',
});
assert.equal(
network.fetched.some((request) => new URL(request.url).origin === 'https://c.example.test'),
false,
);
assert.equal(network.anchored.length, 0);
});
test('a declared instance that serves no discovery is refused', async () => {
network.served.set('https://chat.example.test', officialDocument('https://gone.example.test'));
await assert.rejects(resolveDesktopRuntimePlan({input: 'https://chat.example.test/api', signal: null}), {
name: /DesktopRuntimeDiscovery/,
});
assert.equal(network.anchored.length, 0);
});
});