Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions apps/account-directory/src/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@ function text(value: string, status = 200): Response {
});
}

function withServerTiming(
response: Response,
authDurationMs: number,
dbDurationMs: number,
): Response {
const duration = (value: number) => Math.max(0, value).toFixed(2);
const headers = new Headers(response.headers);
headers.set(
"server-timing",
`auth;dur=${duration(authDurationMs)}, db;dur=${duration(dbDurationMs)}`,
);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}

function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
Expand Down Expand Up @@ -338,14 +356,24 @@ async function handleRegister(request: Request, env: Env, userId: string): Promi
return json(machineRecord(row));
}

async function handleList(request: Request, env: Env, userId: string): Promise<Response> {
async function handleList(
request: Request,
env: Env,
userId: string,
authDurationMs: number,
): Promise<Response> {
if (request.method !== "GET") return text("method not allowed", 405);
const dbStartedAt = performance.now();
const rows = (await env.DB.prepare(`
select user_id, machine_key, device_id, name, platform, device_type, pubkey,
reachable_endpoints, last_seen_at, created_at
from machines
where user_id = ?
-- 500 is the machine-directory cap, matching the client's effective cap.
order by last_seen_at desc
limit 500
`).bind(userId).all<MachineRow>()).results ?? [];
const dbDurationMs = performance.now() - dbStartedAt;
const now = Date.now();
const windowMs = onlineWindowMs(env);
const machines = rows.map((row) => ({
Expand All @@ -355,7 +383,7 @@ async function handleList(request: Request, env: Env, userId: string): Promise<R
if (left.online !== right.online) return left.online ? -1 : 1;
return Number(right.lastSeenAt ?? 0) - Number(left.lastSeenAt ?? 0);
});
return json({ machines });
return withServerTiming(json({ machines }), authDurationMs, dbDurationMs);
}

async function handleDelete(
Expand Down Expand Up @@ -388,6 +416,7 @@ function trustedWebClientOrigin(env: Env): string | null {
function withCors(response: Response, origin: string): Response {
const headers = new Headers(response.headers);
headers.set("access-control-allow-origin", origin);
headers.set("access-control-expose-headers", "Server-Timing");
headers.set("vary", "Origin");
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
}
Expand All @@ -407,19 +436,25 @@ async function handleRequestCore(

const route = routeAccount(url.pathname);
if (!route) return text("not found", 404);
const timeMachineList = route.kind === "list" && request.method === "GET";
const authStartedAt = timeMachineList ? performance.now() : 0;
const authentication = await authenticate(request, env);
const authDurationMs = timeMachineList ? performance.now() - authStartedAt : 0;
if (!authentication.ok) {
return json(
const response = json(
{ error: authentication.reason },
{
status: authentication.reason === "authentication unavailable" ? 503 : 401,
},
);
return timeMachineList
? withServerTiming(response, authDurationMs, 0)
: response;
}
const userId = authentication.userId;

if (route.kind === "register") return await handleRegister(request, env, userId);
if (route.kind === "list") return await handleList(request, env, userId);
if (route.kind === "list") return await handleList(request, env, userId, authDurationMs);
return await handleDelete(request, env, userId, route.machineKey);
}

Expand Down
40 changes: 38 additions & 2 deletions apps/account-directory/test/directory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,17 @@ class FakeD1Database {
}

all<T>(sql: string, values: unknown[]): T[] {
if (!sql.toLowerCase().includes("from machines")) return [];
const normalized = sql.toLowerCase();
if (!normalized.includes("from machines")) return [];
const [userId] = values;
return this.rows.filter((row) => row.user_id === userId) as T[];
let rows = this.rows.filter((row) => row.user_id === userId);
if (normalized.includes("order by last_seen_at desc")) {
rows = [...rows].sort((left, right) =>
Number(right.last_seen_at ?? 0) - Number(left.last_seen_at ?? 0)
);
}
if (normalized.includes("limit 500")) rows = rows.slice(0, 500);
return rows as T[];
}

run(sql: string, values: unknown[]): number {
Expand Down Expand Up @@ -977,6 +985,7 @@ describe("machine directory", () => {
}), env);
expect(allowed.status).toBe(200);
expect(allowed.headers.get("access-control-allow-origin")).toBe("https://app.ade.dev");
expect(allowed.headers.get("access-control-expose-headers")).toBe("Server-Timing");

const hostilePreflight = await handleRequest(new Request("https://directory.test/account/machines", {
method: "OPTIONS",
Expand Down Expand Up @@ -1046,6 +1055,33 @@ describe("machine directory", () => {
lastSeenAt: now - 120_000,
reachableEndpoints: [{ kind: "lan", host: "mac.local", port: 8787 }],
}));
expect(response.headers.get("server-timing")).toMatch(
/^auth;dur=\d+\.\d{2}, db;dur=\d+\.\d{2}$/,
);
});

it("returns only the 500 most recently seen machines", async () => {
const env = makeEnv();
const token = await mintToken({ sub: "user_1" });
env.DB.rows = Array.from({ length: 501 }, (_, index) => ({
user_id: "user_1",
machine_key: `machine-${index}`,
device_id: `device-${index}`,
name: `Machine ${index}`,
platform: "macOS",
device_type: "desktop",
pubkey: null,
reachable_endpoints: "[]",
last_seen_at: index,
created_at: index,
}));

const response = await handleRequest(request("GET", "/account/machines", token), env);
const body = await response.json() as { machines: Array<{ machineKey: string }> };

expect(body.machines).toHaveLength(500);
expect(body.machines[0]?.machineKey).toBe("machine-500");
expect(body.machines[499]?.machineKey).toBe("machine-1");
});

it("honors an environment override for the online window", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
ACCOUNT_MACHINE_HEARTBEAT_MS,
ACCOUNT_MACHINE_RELAY_STATE_POLL_MS,
type AccountMachineRegistrationSnapshot,
buildAccountMachineRegistration,
createBrainAccountMachinePublisherService,
Expand Down Expand Up @@ -125,8 +126,8 @@ afterEach(() => {
});

describe("account machine publisher health", () => {
it("defaults the account-directory heartbeat to 60 seconds", () => {
expect(ACCOUNT_MACHINE_HEARTBEAT_MS).toBe(60_000);
it("defaults the account-directory heartbeat to 30 seconds", () => {
expect(ACCOUNT_MACHINE_HEARTBEAT_MS).toBe(30_000);
});

it("records a successful publication with its endpoint count and timestamps", async () => {
Expand Down Expand Up @@ -319,6 +320,119 @@ describe("account machine publisher health", () => {
expect(fetchImpl).toHaveBeenCalledTimes(2);
service.dispose();
});

it("coalesces relay readiness changes into a publish and resets the heartbeat", async () => {
vi.useFakeTimers();
const current = routeSnapshot();
const fetchImpl = vi.fn(async () => new Response(null, { status: 200 }));
const service = createAccountMachinePublisherService({
getAccessToken: async () => "account-token",
getAccountStatus: () => ({ signedIn: true, sessionReadState: "available" as const }),
getSnapshot: async () => current,
getMachineKey: () => "machine-studio",
directoryBaseUrl: () => "https://directory.example",
fetchImpl,
});

service.start();
await vi.advanceTimersByTimeAsync(0);
expect(fetchImpl).toHaveBeenCalledTimes(1);

current.routeHealth.relay.relayControlConnected = false;
current.routeHealth.relay.relayBridgeValidated = false;
await vi.advanceTimersByTimeAsync(ACCOUNT_MACHINE_RELAY_STATE_POLL_MS);
expect(fetchImpl).toHaveBeenCalledTimes(2);

await vi.advanceTimersByTimeAsync(ACCOUNT_MACHINE_HEARTBEAT_MS - 1);
expect(fetchImpl).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(1);
expect(fetchImpl).toHaveBeenCalledTimes(3);
service.dispose();
});

it("publishes at the first relay poll when the startup snapshot was unavailable", async () => {
vi.useFakeTimers();
let ready = false;
const fetchImpl = vi.fn(async () => new Response(null, { status: 200 }));
const service = createAccountMachinePublisherService({
getAccessToken: async () => "account-token",
getAccountStatus: () => ({ signedIn: true, sessionReadState: "available" as const }),
getSnapshot: async () => ready ? routeSnapshot() : null,
getMachineKey: () => "machine-studio",
directoryBaseUrl: () => "https://directory.example",
fetchImpl,
});

service.start();
await vi.advanceTimersByTimeAsync(0);
expect(service.getPublisherHealth().state).toBe("no_active_sync_scope");
expect(fetchImpl).not.toHaveBeenCalled();

ready = true;
await vi.advanceTimersByTimeAsync(ACCOUNT_MACHINE_RELAY_STATE_POLL_MS);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(service.getPublisherHealth().state).toBe("published");
service.dispose();
});

it("publishes when the reachable endpoint set changes", async () => {
vi.useFakeTimers();
const current = snapshot();
const fetchImpl = vi.fn(async (
_input: string | URL | Request,
_init?: RequestInit,
) => new Response(null, { status: 200 }));
const service = createAccountMachinePublisherService({
getAccessToken: async () => "account-token",
getAccountStatus: () => ({ signedIn: true, sessionReadState: "available" as const }),
getSnapshot: async () => current,
getMachineKey: () => "machine-studio",
directoryBaseUrl: () => "https://directory.example",
fetchImpl,
});

service.start();
await vi.advanceTimersByTimeAsync(0);
current.pairingConnectInfo!.addressCandidates.push({
kind: "lan",
host: "192.168.1.21",
});
await vi.advanceTimersByTimeAsync(ACCOUNT_MACHINE_RELAY_STATE_POLL_MS);

expect(fetchImpl).toHaveBeenCalledTimes(2);
const registration = JSON.parse(String(fetchImpl.mock.calls[1]?.[1]?.body));
expect(registration.reachableEndpoints).toEqual([
{ kind: "lan", host: "192.168.1.20", port: 8787 },
{ kind: "lan", host: "192.168.1.21", port: 8787 },
]);
expect(service.getPublisherHealth().reachableEndpointCount).toBe(2);
service.dispose();
});

it("does not publish relay transitions while signed out", async () => {
vi.useFakeTimers();
const current = routeSnapshot();
const getAccessToken = vi.fn(async () => "account-token");
const fetchImpl = vi.fn(async () => new Response(null, { status: 200 }));
const service = createAccountMachinePublisherService({
getAccessToken,
getAccountStatus: () => ({ signedIn: false, sessionReadState: "missing" as const }),
getSnapshot: async () => current,
getMachineKey: () => "machine-studio",
directoryBaseUrl: () => "https://directory.example",
fetchImpl,
});

service.start();
await vi.advanceTimersByTimeAsync(0);
current.routeHealth.relay.relayBridgeValidated = false;
await vi.advanceTimersByTimeAsync(ACCOUNT_MACHINE_RELAY_STATE_POLL_MS * 2);

expect(getAccessToken).not.toHaveBeenCalled();
expect(fetchImpl).not.toHaveBeenCalled();
expect(service.getPublisherHealth().state).toBe("account_signed_out");
service.dispose();
});
});

describe("brain account machine publisher directory policy", () => {
Expand Down
Loading
Loading