Compare commits

..
4 changed files with 124 additions and 5 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);
});
});
@@ -98,6 +98,17 @@ describe('renderTrayIcon', () => {
assert.deepEqual(pixel(bitmap, 32, 6, 16), [0xff, 0xff, 0xff, 0xff]);
});
test('keeps the Linux monochrome pixmap within 256 pixels', () => {
const result = renderTrayIcon(FakeNativeImage.fromBitmap(256), 'white', false, 'linux');
const [representation] = result.representations.values();
assert.equal(representation.width, 256);
assert.equal(representation.height, 256);
const bitmap = result.toBitmap();
assert.deepEqual(pixel(bitmap, 256, 128, 50), [0xff, 0xff, 0xff, 0xff]);
assert.deepEqual(pixel(bitmap, 256, 128, 128), [0, 0, 0, 0]);
assert.deepEqual(pixel(bitmap, 256, 20, 128), [0, 0, 0, 0]);
});
test('skips representations whose bitmap does not match their size', () => {
const base = FakeNativeImage.fromBitmap(16);
base.toBitmap = () => Buffer.alloc(3);
+38 -1
View File
@@ -18,6 +18,7 @@ const MIN_GLYPH_CONTRAST = 32;
const GLYPH_CUTOUT_START = 0.1;
const GLYPH_CUTOUT_END = 0.5;
const LINUX_MONOCHROME_CANVAS_RATIO = 22 / 16;
const LINUX_MAX_PIXMAP_SIZE = 256;
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const DOT_BGR: Bgr = [0x00, 0x00, 0xdd];
const TEMPLATE_DOT_BGR: Bgr = [0x00, 0x00, 0x00];
@@ -149,6 +150,36 @@ function cropTrayGlyph(buffer: Buffer, width: number, height: number): TrayBitma
return {buffer: target, width: size, height: size};
}
function downscaleTrayBitmap(bitmap: TrayBitmap, size: number): TrayBitmap {
if (bitmap.width <= size) return bitmap;
const scale = bitmap.width / size;
const buffer = Buffer.alloc(size * size * 4);
const sums = [0, 0, 0, 0];
for (let y = 0; y < size; y++) {
const top = y * scale;
const bottom = top + scale;
for (let x = 0; x < size; x++) {
const left = x * scale;
const right = left + scale;
sums.fill(0);
for (let sy = Math.floor(top); sy < Math.min(bitmap.height, Math.ceil(bottom)); sy++) {
const weightY = Math.min(bottom, sy + 1) - Math.max(top, sy);
for (let sx = Math.floor(left); sx < Math.min(bitmap.width, Math.ceil(right)); sx++) {
const weight = weightY * (Math.min(right, sx + 1) - Math.max(left, sx));
const offset = (sy * bitmap.width + sx) * 4;
for (let channel = 0; channel < 4; channel++) {
sums[channel] += bitmap.buffer[offset + channel] * weight;
}
}
}
for (let channel = 0; channel < 4; channel++) {
buffer[(y * size + x) * 4 + channel] = Math.round(sums[channel] / (scale * scale));
}
}
}
return {buffer, width: size, height: size};
}
function addMonochromeTrayMargins(bitmap: TrayBitmap, size: number): TrayBitmap {
const inset = Math.floor((size - bitmap.width) / 2);
const buffer = Buffer.alloc(size * size * 4);
@@ -210,7 +241,13 @@ export function renderTrayIcon(
appearance === 'template' ? TEMPLATE_DOT_BGR : DOT_BGR,
);
}
if (linuxMonochrome) bitmap = addMonochromeTrayMargins(bitmap, Math.ceil(logicalCanvasSize * scaleFactor));
if (linuxMonochrome) {
const canvasSize = Math.min(Math.ceil(logicalCanvasSize * scaleFactor), LINUX_MAX_PIXMAP_SIZE);
bitmap = addMonochromeTrayMargins(
downscaleTrayBitmap(bitmap, Math.floor(canvasSize / LINUX_MONOCHROME_CANVAS_RATIO)),
canvasSize,
);
}
result.addRepresentation({scaleFactor, ...bitmap});
}
if (result.isEmpty()) {