源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@openwork/testkit",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./stack": "./src/stack.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openwork/behaviors": "workspace:*",
|
||||
"@openwork/cdp": "workspace:*",
|
||||
"@openwork/env": "workspace:*",
|
||||
"@openwork/test-evidence": "workspace:*",
|
||||
"@openwork/hosts": "workspace:*",
|
||||
"@openwork/labs": "workspace:*",
|
||||
"mysql2": "^3.11.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { resolvePlace } from "@openwork/env";
|
||||
|
||||
export interface TestContext {
|
||||
place: ReturnType<typeof resolvePlace>;
|
||||
evidence: unknown;
|
||||
skip(note?: string): never;
|
||||
}
|
||||
|
||||
export type RegisterTest = (title: string, fn: (ctx: TestContext) => Promise<void> | void) => void;
|
||||
|
||||
let registerTest: RegisterTest | undefined;
|
||||
|
||||
export function setBriefTestRegistrar(registrar: RegisterTest): void {
|
||||
registerTest = registrar;
|
||||
}
|
||||
|
||||
export function getBriefTestRegistrar(): RegisterTest {
|
||||
if (!registerTest) throw new Error("briefTest must be imported from @openwork/testkit.");
|
||||
return registerTest;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { currentTestEvidence } from "@openwork/test-evidence";
|
||||
import { getBriefTestRegistrar } from "./brief-internal.ts";
|
||||
import type { TestContext } from "./brief-internal.ts";
|
||||
|
||||
export interface BriefClaim {
|
||||
must: string;
|
||||
never?: string;
|
||||
}
|
||||
|
||||
export function claim(must: string, opts: { never?: string } = {}): BriefClaim {
|
||||
return { must, ...opts };
|
||||
}
|
||||
|
||||
export interface TestBrief<K extends string> {
|
||||
behavior: string;
|
||||
claims: Record<K, BriefClaim>;
|
||||
}
|
||||
|
||||
export function testBrief<K extends string>(def: TestBrief<K>): TestBrief<K> {
|
||||
if (!def.behavior.trim()) throw new Error("Test brief behavior must not be empty.");
|
||||
const keys = Object.keys(def.claims);
|
||||
if (keys.length === 0) throw new Error("Test brief must declare at least one claim.");
|
||||
for (const key in def.claims) {
|
||||
const value = def.claims[key];
|
||||
if (!value.must.trim()) throw new Error(`Test brief claim "${key}" must not have a blank must.`);
|
||||
if (value.never !== undefined && !value.never.trim()) {
|
||||
throw new Error(`Test brief claim "${key}" must not have a blank never.`);
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
export type Prove<K extends string> = Record<K, (passed: boolean, evidence: string) => void>;
|
||||
|
||||
export interface BriefRun<K extends string> {
|
||||
prove: Prove<K>;
|
||||
assertAllProven(): void;
|
||||
facts(): { key: K; passed: boolean; evidence: string }[];
|
||||
}
|
||||
|
||||
export function createBriefRun<K extends string>(
|
||||
brief: TestBrief<K>,
|
||||
record: (claimText: string, evidence: string, passed: boolean) => void = (claimText, evidence, passed) => {
|
||||
currentTestEvidence()?.recordAssertionEvidence(claimText, evidence, passed);
|
||||
},
|
||||
): BriefRun<K> {
|
||||
const latest = new Map<K, { key: K; passed: boolean; evidence: string }>();
|
||||
const prove: Prove<K> = Object.create(null);
|
||||
|
||||
for (const key in brief.claims) {
|
||||
const value = brief.claims[key];
|
||||
prove[key] = (passed, evidence) => {
|
||||
if (!evidence.trim()) throw new Error(`Proof evidence for claim "${key}" must not be blank.`);
|
||||
const claimText = `${key}: ${value.must}${value.never === undefined ? "" : ` — never: ${value.never}`}`;
|
||||
record(claimText, evidence, passed);
|
||||
latest.set(key, { key, passed, evidence });
|
||||
if (passed !== true) throw new Error(`Claim failed: ${key} — ${value.must}. Evidence: ${evidence}`);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prove,
|
||||
assertAllProven() {
|
||||
const missing: K[] = [];
|
||||
for (const key in brief.claims) {
|
||||
if (!latest.has(key)) missing.push(key);
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Brief claims left unproven: ${missing.join(", ")}. Every declared claim needs a prove.<claim>() call.`);
|
||||
}
|
||||
},
|
||||
facts: () => [...latest.values()],
|
||||
};
|
||||
}
|
||||
|
||||
interface BriefTestContext<K extends string> {
|
||||
prove: Prove<K>;
|
||||
place: TestContext["place"];
|
||||
evidence: TestContext["evidence"];
|
||||
skip: TestContext["skip"];
|
||||
}
|
||||
|
||||
function briefTitle(behavior: string): string {
|
||||
const sentence = behavior.split(".", 1)[0].trim();
|
||||
return sentence.length <= 100 ? sentence : `${sentence.slice(0, 97)}...`;
|
||||
}
|
||||
|
||||
export function briefTest<K extends string>(
|
||||
brief: TestBrief<K>,
|
||||
fn: (ctx: BriefTestContext<K>) => Promise<void> | void,
|
||||
): void {
|
||||
getBriefTestRegistrar()(briefTitle(brief.behavior), async ({ place, evidence, skip }) => {
|
||||
const run = createBriefRun(brief);
|
||||
await fn({ prove: run.prove, place, evidence, skip });
|
||||
run.assertAllProven();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface EventuallyOptions<T> {
|
||||
within: number;
|
||||
intervalMs?: number;
|
||||
label?: string;
|
||||
until?: (value: T) => boolean;
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function describe(value: unknown): string {
|
||||
try {
|
||||
const json = JSON.stringify(value);
|
||||
return json === undefined ? String(value) : json;
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export async function eventually<T>(
|
||||
probe: () => Promise<T> | T,
|
||||
options: EventuallyOptions<T>,
|
||||
): Promise<T> {
|
||||
const intervalMs = options.intervalMs ?? 500;
|
||||
const until = options.until ?? ((value: T) => Boolean(value));
|
||||
const deadline = Date.now() + options.within;
|
||||
let lastValue: T | undefined;
|
||||
let lastError: unknown;
|
||||
let hasValue = false;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
lastValue = await probe();
|
||||
hasValue = true;
|
||||
lastError = undefined;
|
||||
if (until(lastValue)) return lastValue;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
|
||||
const remaining = deadline - Date.now();
|
||||
if (remaining <= 0) {
|
||||
const label = options.label ?? "condition";
|
||||
const valueDetail = hasValue ? `; last value: ${describe(lastValue)}` : "";
|
||||
const errorDetail = lastError === undefined ? "" : `; last error: ${describe(lastError instanceof Error ? lastError.message : lastError)}`;
|
||||
throw new Error(`Timed out waiting for ${label} after ${options.within}ms${valueDetail}${errorDetail}`);
|
||||
}
|
||||
await sleep(Math.min(intervalMs, remaining));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { test as evidenceTest } from "@openwork/test-evidence/vitest";
|
||||
import { SkipError, resolvePlace } from "@openwork/env";
|
||||
import { setBriefTestRegistrar } from "./brief-internal.ts";
|
||||
|
||||
const fixtureTest = evidenceTest.extend<{ place: ReturnType<typeof resolvePlace> }>({
|
||||
place: [async ({}, use) => {
|
||||
await use(resolvePlace(process.env));
|
||||
}, { auto: true }],
|
||||
});
|
||||
|
||||
interface WrappedContext {
|
||||
place: ReturnType<typeof resolvePlace>;
|
||||
evidence: unknown;
|
||||
skip(note?: string): never;
|
||||
}
|
||||
|
||||
// Vitest does not propagate a test-body error back through fixture `use()`, so
|
||||
// the exported test boundary is where SkipError can reliably become ctx.skip().
|
||||
function wrapTestApi<T extends (...args: never[]) => unknown>(api: T): T {
|
||||
return new Proxy(api, {
|
||||
apply(target, thisArg, argArray) {
|
||||
const args = [...argArray];
|
||||
const callback = args.at(-1);
|
||||
if (typeof callback === "function") {
|
||||
args[args.length - 1] = async ({ place, evidence, skip }: WrappedContext) => {
|
||||
try {
|
||||
return await Reflect.apply(callback, undefined, [{ place, evidence, skip }]);
|
||||
} catch (error) {
|
||||
if (!(error instanceof SkipError)) throw error;
|
||||
return skip(`needs: ${error.reason}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
return Reflect.apply(target, thisArg, args);
|
||||
},
|
||||
get(target, property, receiver) {
|
||||
const value = Reflect.get(target, property, receiver);
|
||||
if (typeof value !== "function") return value;
|
||||
return (...args: unknown[]) => {
|
||||
const result = Reflect.apply(value, target, args);
|
||||
return typeof result === "function" ? wrapTestApi(result) : result;
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const test = wrapTestApi(fixtureTest);
|
||||
|
||||
setBriefTestRegistrar((title, fn) => {
|
||||
test(title, fn);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export { createDesktopHandoffGrant, signInDesktopAs } from "@openwork/behaviors";
|
||||
export type { DesktopHandle } from "@openwork/hosts";
|
||||
export { test } from "./fixture.ts";
|
||||
export * from "@openwork/env";
|
||||
export * from "./brief.ts";
|
||||
export * from "./eventually.ts";
|
||||
export * from "./link.ts";
|
||||
export * from "./self-host.ts";
|
||||
export * from "./state.ts";
|
||||
@@ -0,0 +1,414 @@
|
||||
import { createServer, request as httpRequest } from "node:http";
|
||||
import { request as httpsRequest } from "node:https";
|
||||
import { createHash, timingSafeEqual } from "node:crypto";
|
||||
import { clearTimeout, setTimeout } from "node:timers";
|
||||
import { URL } from "node:url";
|
||||
|
||||
const HOP_BY_HOP_HEADERS = new Set([
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"proxy-connection",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]);
|
||||
|
||||
function cliValue(name, fallback) {
|
||||
const index = process.argv.indexOf(`--${name}`);
|
||||
return index >= 0 ? process.argv[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function requiredPort(value, name) {
|
||||
const port = Number(value);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error(`${name} must be a valid port.`);
|
||||
return port;
|
||||
}
|
||||
|
||||
const upstream = new URL(cliValue("upstream", process.env.LINK_UPSTREAM));
|
||||
const dataPort = requiredPort(cliValue("port", process.env.LINK_PORT), "port");
|
||||
const adminPort = requiredPort(cliValue("admin-port", process.env.LINK_ADMIN_PORT), "admin-port");
|
||||
const adminToken = process.env.LINK_ADMIN_TOKEN ?? cliValue("admin-token");
|
||||
if (upstream.protocol !== "http:" && upstream.protocol !== "https:") throw new Error("upstream must use http or https.");
|
||||
if (typeof adminToken !== "string" || !/^[a-f0-9]{32,}$/.test(adminToken)) throw new Error("admin token must be at least 32 hex characters.");
|
||||
const expectedAuthorizationHash = createHash("sha256").update(`Bearer ${adminToken}`).digest();
|
||||
|
||||
let phase = "default";
|
||||
let profile = "baseline";
|
||||
let rules = [];
|
||||
let bytesPerSec = null;
|
||||
let offline = false;
|
||||
let offlineUntil = null;
|
||||
let offlineTimer = null;
|
||||
let shuttingDown = false;
|
||||
const requests = [];
|
||||
const refusedConnections = Object.create(null);
|
||||
const dataSockets = new Set();
|
||||
const adminSockets = new Set();
|
||||
const stalledSockets = new Set();
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function headerText(value) {
|
||||
return Array.isArray(value) ? value.join(",") : value ?? "";
|
||||
}
|
||||
|
||||
function forwardedHeaders(source, host) {
|
||||
const nominated = new Set(headerText(source.connection).split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
|
||||
const headers = {};
|
||||
for (const [name, value] of Object.entries(source)) {
|
||||
if (value === undefined || HOP_BY_HOP_HEADERS.has(name) || nominated.has(name)) continue;
|
||||
headers[name] = value;
|
||||
}
|
||||
if (host) headers.host = host;
|
||||
return headers;
|
||||
}
|
||||
|
||||
function nonNegativeNumber(value, name) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative number.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveInteger(value, name) {
|
||||
if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeRule(value) {
|
||||
if (!isRecord(value) || typeof value.kind !== "string") throw new Error("Each rule must be an object with a kind.");
|
||||
const pathPrefix = value.pathPrefix === undefined ? "/" : value.pathPrefix;
|
||||
if (typeof pathPrefix !== "string") throw new Error("Rule pathPrefix must be a string.");
|
||||
const everyNth = value.everyNth === undefined ? null : positiveInteger(value.everyNth, "Rule everyNth");
|
||||
let remaining = everyNth === null ? 1 : Infinity;
|
||||
if (value.times !== undefined) {
|
||||
if (!Number.isInteger(value.times) || value.times < 0) throw new Error("Rule times must be a non-negative integer.");
|
||||
remaining = value.times;
|
||||
}
|
||||
const base = { kind: value.kind, pathPrefix, everyNth, remaining, matches: 0 };
|
||||
if (value.kind === "latency") {
|
||||
return { ...base, delayMs: nonNegativeNumber(value.delayMs, "Latency delayMs"), jitterMs: nonNegativeNumber(value.jitterMs ?? 0, "Latency jitterMs") };
|
||||
}
|
||||
if (value.kind === "status") {
|
||||
const statusCode = positiveInteger(value.statusCode, "Status statusCode");
|
||||
if (statusCode > 999) throw new Error("Status statusCode must be at most 999.");
|
||||
return { ...base, statusCode, body: value.body };
|
||||
}
|
||||
if (value.kind === "reset" || value.kind === "stall") return base;
|
||||
throw new Error(`Unsupported rule kind: ${value.kind}`);
|
||||
}
|
||||
|
||||
function takeRule(path) {
|
||||
for (const rule of rules) {
|
||||
if (rule.remaining <= 0 || !path.startsWith(rule.pathPrefix)) continue;
|
||||
rule.matches += 1;
|
||||
if (rule.everyNth !== null && rule.matches % rule.everyNth !== 0) continue;
|
||||
if (Number.isFinite(rule.remaining)) rule.remaining -= 1;
|
||||
return rule;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function requestEntry(incoming, path, status, fault) {
|
||||
requests.push({
|
||||
method: incoming.method ?? "GET",
|
||||
path,
|
||||
status,
|
||||
faulted: fault !== null,
|
||||
...(fault === null ? {} : { fault }),
|
||||
phase,
|
||||
profile,
|
||||
at: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
function writeUpstreamResponse(client, response) {
|
||||
const status = response.statusCode ?? 502;
|
||||
const headers = forwardedHeaders(response.headers);
|
||||
if (response.statusMessage) client.writeHead(status, response.statusMessage, headers);
|
||||
else client.writeHead(status, headers);
|
||||
}
|
||||
|
||||
async function pacedPipe(source, destination, rate) {
|
||||
const chunkSize = Math.max(1, Math.min(Math.floor(rate / 10), 64 * 1024));
|
||||
const intervalMs = 1_000 * chunkSize / rate;
|
||||
for await (const value of source) {
|
||||
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
||||
for (let offset = 0; offset < chunk.length; offset += chunkSize) {
|
||||
destination.write(chunk.subarray(offset, Math.min(offset + chunkSize, chunk.length)));
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
destination.end();
|
||||
}
|
||||
|
||||
function forward(incoming, client, path, fault) {
|
||||
const requested = new URL(path, "http://request-target.invalid");
|
||||
const options = {
|
||||
protocol: upstream.protocol,
|
||||
hostname: upstream.hostname,
|
||||
port: upstream.port,
|
||||
path: `${requested.pathname}${requested.search}`,
|
||||
method: incoming.method ?? "GET",
|
||||
headers: forwardedHeaders(incoming.headers, upstream.host),
|
||||
};
|
||||
const onResponse = (response) => {
|
||||
const status = response.statusCode ?? 502;
|
||||
requestEntry(incoming, path, status, fault ?? (bytesPerSec === null ? null : "bandwidth"));
|
||||
writeUpstreamResponse(client, response);
|
||||
const rate = bytesPerSec;
|
||||
if (rate === null) {
|
||||
response.pipe(client);
|
||||
return;
|
||||
}
|
||||
void pacedPipe(response, client, rate).catch((error) => client.destroy(error));
|
||||
};
|
||||
const outbound = upstream.protocol === "https:"
|
||||
? httpsRequest(options, onResponse)
|
||||
: httpRequest(options, onResponse);
|
||||
outbound.on("error", (error) => {
|
||||
if (client.destroyed) return;
|
||||
if (client.headersSent) {
|
||||
client.destroy(error);
|
||||
return;
|
||||
}
|
||||
requestEntry(incoming, path, 502, fault);
|
||||
client.writeHead(502, { "content-type": "application/json" });
|
||||
client.end(JSON.stringify({ error: error.message }));
|
||||
});
|
||||
incoming.on("aborted", () => outbound.destroy());
|
||||
client.on("close", () => outbound.destroy());
|
||||
incoming.pipe(outbound);
|
||||
}
|
||||
|
||||
function destroyStalledSockets() {
|
||||
for (const socket of stalledSockets) socket.destroy();
|
||||
stalledSockets.clear();
|
||||
}
|
||||
|
||||
function restoreOnline() {
|
||||
if (offlineTimer !== null) clearTimeout(offlineTimer);
|
||||
offlineTimer = null;
|
||||
offline = false;
|
||||
offlineUntil = null;
|
||||
}
|
||||
|
||||
function activateOffline(durationMs) {
|
||||
restoreOnline();
|
||||
offline = true;
|
||||
offlineUntil = Date.now() + durationMs;
|
||||
for (const socket of dataSockets) socket.destroy();
|
||||
stalledSockets.clear();
|
||||
offlineTimer = setTimeout(restoreOnline, durationMs);
|
||||
}
|
||||
|
||||
const dataServer = createServer((incoming, response) => {
|
||||
void (async () => {
|
||||
const path = incoming.url ?? "/";
|
||||
const rule = takeRule(path);
|
||||
if (rule?.kind === "status") {
|
||||
const body = JSON.stringify(rule.body ?? { error: `Injected HTTP ${rule.statusCode}` });
|
||||
requestEntry(incoming, path, rule.statusCode, "status");
|
||||
response.writeHead(rule.statusCode, {
|
||||
"access-control-allow-origin": "*",
|
||||
"content-length": Buffer.byteLength(body),
|
||||
"content-type": "application/json",
|
||||
});
|
||||
response.end(body);
|
||||
return;
|
||||
}
|
||||
if (rule?.kind === "reset") {
|
||||
requestEntry(incoming, path, 0, "reset");
|
||||
response.destroy();
|
||||
return;
|
||||
}
|
||||
if (rule?.kind === "stall") {
|
||||
requestEntry(incoming, path, 0, "stall");
|
||||
if (incoming.socket) {
|
||||
stalledSockets.add(incoming.socket);
|
||||
incoming.socket.once("close", () => stalledSockets.delete(incoming.socket));
|
||||
}
|
||||
incoming.resume();
|
||||
return;
|
||||
}
|
||||
if (rule?.kind === "latency") {
|
||||
const span = Math.floor(rule.jitterMs * 2) + 1;
|
||||
const jitter = span > 1 ? (rule.matches * 1103515245 + 12345) % span - Math.floor(rule.jitterMs) : 0;
|
||||
await sleep(Math.max(0, rule.delayMs + jitter));
|
||||
}
|
||||
if (response.destroyed) return;
|
||||
forward(incoming, response, path, rule === null ? null : "latency");
|
||||
})().catch((error) => {
|
||||
if (response.destroyed) return;
|
||||
if (response.headersSent) {
|
||||
response.destroy(error instanceof Error ? error : undefined);
|
||||
return;
|
||||
}
|
||||
response.writeHead(500, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
||||
});
|
||||
});
|
||||
|
||||
dataServer.prependListener("connection", (socket) => {
|
||||
if (offline) {
|
||||
refusedConnections[phase] = (refusedConnections[phase] ?? 0) + 1;
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
dataSockets.add(socket);
|
||||
socket.once("close", () => {
|
||||
dataSockets.delete(socket);
|
||||
stalledSockets.delete(socket);
|
||||
});
|
||||
});
|
||||
|
||||
function readJson(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let length = 0;
|
||||
request.on("data", (chunk) => {
|
||||
length += chunk.length;
|
||||
if (length > 1024 * 1024) {
|
||||
reject(new Error("Admin request body is too large."));
|
||||
request.destroy();
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
request.on("end", () => {
|
||||
try {
|
||||
resolve(chunks.length === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendJson(response, status, body) {
|
||||
const text = JSON.stringify(body);
|
||||
response.writeHead(status, { "content-length": Buffer.byteLength(text), "content-type": "application/json" });
|
||||
response.end(text);
|
||||
}
|
||||
|
||||
function isAuthorized(request) {
|
||||
const receivedHash = createHash("sha256").update(headerText(request.headers.authorization)).digest();
|
||||
return timingSafeEqual(receivedHash, expectedAuthorizationHash);
|
||||
}
|
||||
|
||||
const adminServer = createServer((request, response) => {
|
||||
void (async () => {
|
||||
if (!isAuthorized(request)) {
|
||||
sendJson(response, 401, { error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
const path = new URL(request.url ?? "/", "http://admin.invalid").pathname;
|
||||
if (request.method === "GET" && path === "/health") {
|
||||
sendJson(response, 200, { ok: true, upstream: upstream.href, phase, profile, offline, offlineUntil, rules: rules.length });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && path === "/requests") {
|
||||
sendJson(response, 200, { requests, refusedConnections, phase, profile });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && path === "/stats") {
|
||||
sendJson(response, 200, {
|
||||
requests: requests.length,
|
||||
faults: requests.filter((entry) => entry.faulted).length,
|
||||
refusedConnections: Object.values(refusedConnections).reduce((total, count) => total + count, 0),
|
||||
phase,
|
||||
profile,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method !== "POST") {
|
||||
sendJson(response, 404, { error: "Not found" });
|
||||
return;
|
||||
}
|
||||
const body = await readJson(request);
|
||||
if (!isRecord(body)) throw new Error("Admin body must be a JSON object.");
|
||||
if (path === "/phase") {
|
||||
if (typeof body.name !== "string" || body.name.length === 0) throw new Error("phase name must be a non-empty string.");
|
||||
phase = body.name;
|
||||
if (body.profile !== undefined) {
|
||||
if (body.profile !== "baseline" && body.profile !== "vpn-flaky-emulated") throw new Error("profile must be baseline or vpn-flaky-emulated.");
|
||||
profile = body.profile;
|
||||
if (profile === "baseline") {
|
||||
rules = [];
|
||||
bytesPerSec = null;
|
||||
} else {
|
||||
// Check the periodic rekey-style reset first. Non-reset requests then
|
||||
// fall through to the catch-all 300–800ms latency rule.
|
||||
rules = [
|
||||
normalizeRule({ kind: "reset", everyNth: 3 }),
|
||||
normalizeRule({ kind: "latency", delayMs: 550, jitterMs: 250, everyNth: 1 }),
|
||||
];
|
||||
bytesPerSec = 256 * 1024;
|
||||
}
|
||||
}
|
||||
} else if (path === "/rules") {
|
||||
if (!Array.isArray(body.rules)) throw new Error("rules must be an array.");
|
||||
rules = body.rules.map(normalizeRule);
|
||||
} else if (path === "/bandwidth") {
|
||||
if (body.bytesPerSec === null) bytesPerSec = null;
|
||||
else bytesPerSec = positiveInteger(body.bytesPerSec, "bytesPerSec");
|
||||
} else if (path === "/offline") {
|
||||
activateOffline(nonNegativeNumber(body.durationMs, "durationMs"));
|
||||
} else if (path === "/clear") {
|
||||
rules = [];
|
||||
bytesPerSec = null;
|
||||
profile = "baseline";
|
||||
restoreOnline();
|
||||
destroyStalledSockets();
|
||||
} else {
|
||||
sendJson(response, 404, { error: "Not found" });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, { ok: true });
|
||||
})().catch(() => {
|
||||
if (!response.headersSent) sendJson(response, 400, { error: "Invalid request" });
|
||||
else response.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
adminServer.on("connection", (socket) => {
|
||||
adminSockets.add(socket);
|
||||
socket.once("close", () => adminSockets.delete(socket));
|
||||
});
|
||||
|
||||
function listen(server, port, host) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(port, host, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function close(server) {
|
||||
return new Promise((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
|
||||
async function shutdown() {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
restoreOnline();
|
||||
for (const socket of dataSockets) socket.destroy();
|
||||
for (const socket of adminSockets) socket.destroy();
|
||||
await Promise.all([close(dataServer), close(adminServer)]);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", () => void shutdown().then(() => process.exit(0)));
|
||||
process.on("SIGINT", () => void shutdown().then(() => process.exit(0)));
|
||||
|
||||
await Promise.all([
|
||||
listen(dataServer, dataPort, "0.0.0.0"),
|
||||
listen(adminServer, adminPort, "0.0.0.0"),
|
||||
]);
|
||||
console.log(`link-server listening data=${dataPort} admin=${adminPort} upstream=${upstream.href}`);
|
||||
@@ -0,0 +1,407 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { allocateFreePort } from "@openwork/cdp";
|
||||
import { checkedExec, defaultDaytonaExec } from "@openwork/hosts";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import type { DenRef } from "@openwork/behaviors";
|
||||
import type { DaytonaExec } from "@openwork/hosts";
|
||||
|
||||
const DAYTONA_LINK_SCRIPT = "/tmp/openwork-den-link-server.mjs";
|
||||
const DAYTONA_LINK_PID = "/tmp/openwork-den-link-server.pid";
|
||||
const DAYTONA_LINK_BASE64 = `${DAYTONA_LINK_SCRIPT}.b64`;
|
||||
const MAX_LINK_SCRIPT_BYTES = 512 * 1024;
|
||||
const DAYTONA_LINK_BASE64_CHUNK_LENGTH = 8 * 1024;
|
||||
/** Conservative ceiling for each complete Daytona argv command string. */
|
||||
export const MAX_LINK_DAYTONA_COMMAND_LENGTH = 12 * 1024;
|
||||
|
||||
export type LinkRule = { pathPrefix?: string; times?: number; everyNth?: number } & (
|
||||
| { kind: "latency"; delayMs: number; jitterMs?: number }
|
||||
| { kind: "status"; statusCode: number; body?: unknown }
|
||||
| { kind: "reset" }
|
||||
| { kind: "stall" }
|
||||
);
|
||||
|
||||
export type LinkProfile = "baseline" | "vpn-flaky-emulated";
|
||||
export type DenLinkClient = "public-preview" | "sandbox-loopback";
|
||||
|
||||
export interface LinkRequestEntry {
|
||||
method: string;
|
||||
path: string;
|
||||
status: number;
|
||||
faulted: boolean;
|
||||
fault?: string;
|
||||
phase: string;
|
||||
profile: LinkProfile;
|
||||
at: number;
|
||||
}
|
||||
|
||||
export interface LinkLog {
|
||||
requests: LinkRequestEntry[];
|
||||
refusedConnections: Record<string, number>;
|
||||
phase: string;
|
||||
profile: LinkProfile;
|
||||
}
|
||||
|
||||
export interface LinkStats {
|
||||
requests: number;
|
||||
faults: number;
|
||||
refusedConnections: number;
|
||||
phase: string;
|
||||
profile: LinkProfile;
|
||||
}
|
||||
|
||||
export interface DenLink extends AsyncDisposable {
|
||||
/** Den ref as seen by the app: webUrl is the shaped data URL. */
|
||||
ref: DenRef;
|
||||
admin: {
|
||||
phase(name: string, profile?: LinkProfile): Promise<void>;
|
||||
rules(rules: LinkRule[]): Promise<void>;
|
||||
bandwidth(bytesPerSec: number | null): Promise<void>;
|
||||
offline(durationMs: number): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
requests(): Promise<LinkLog>;
|
||||
stats(): Promise<LinkStats>;
|
||||
health(): Promise<{ ok: boolean; phase: string; offline: boolean }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DenLinkOptions {
|
||||
/** Run in this known Daytona sandbox, normally `den.placement.sandboxId`. Omit for a local child process. */
|
||||
sandboxId?: string;
|
||||
/** URL exposed to the app for Daytona placement. Defaults to the public data preview. */
|
||||
client?: DenLinkClient;
|
||||
/** Fixed in-sandbox ports; defaults 3985 (data) / 3986 (admin). Local placement allocates free ports and ignores these. */
|
||||
port?: number;
|
||||
adminPort?: number;
|
||||
/** Override Daytona command execution, primarily for deterministic tests. */
|
||||
daytonaExec?: DaytonaExec;
|
||||
}
|
||||
|
||||
interface LinkPlacement {
|
||||
dataUrl: string;
|
||||
adminUrl: string;
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function messageText(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function isProfile(value: unknown): value is LinkProfile {
|
||||
return value === "baseline" || value === "vpn-flaky-emulated";
|
||||
}
|
||||
|
||||
function parseHealth(value: unknown): { ok: boolean; phase: string; offline: boolean } {
|
||||
if (isRecord(value) && typeof value.ok === "boolean" && typeof value.phase === "string" && typeof value.offline === "boolean") {
|
||||
return { ok: value.ok, phase: value.phase, offline: value.offline };
|
||||
}
|
||||
throw new Error("Link health response has an invalid shape.");
|
||||
}
|
||||
|
||||
function parseRequestEntry(value: unknown): LinkRequestEntry {
|
||||
if (!isRecord(value)
|
||||
|| typeof value.method !== "string"
|
||||
|| typeof value.path !== "string"
|
||||
|| typeof value.status !== "number"
|
||||
|| typeof value.faulted !== "boolean"
|
||||
|| (value.fault !== undefined && typeof value.fault !== "string")
|
||||
|| typeof value.phase !== "string"
|
||||
|| !isProfile(value.profile)
|
||||
|| typeof value.at !== "number") {
|
||||
throw new Error("Link request entry has an invalid shape.");
|
||||
}
|
||||
return {
|
||||
method: value.method,
|
||||
path: value.path,
|
||||
status: value.status,
|
||||
faulted: value.faulted,
|
||||
...(value.fault === undefined ? {} : { fault: value.fault }),
|
||||
phase: value.phase,
|
||||
profile: value.profile,
|
||||
at: value.at,
|
||||
};
|
||||
}
|
||||
|
||||
function parseLog(value: unknown): LinkLog {
|
||||
if (!isRecord(value) || !Array.isArray(value.requests) || !isRecord(value.refusedConnections) || typeof value.phase !== "string" || !isProfile(value.profile)) {
|
||||
throw new Error("Link requests response has an invalid shape.");
|
||||
}
|
||||
const refusedConnections: Record<string, number> = {};
|
||||
for (const [name, count] of Object.entries(value.refusedConnections)) {
|
||||
if (typeof count !== "number") throw new Error("Link refusal count has an invalid shape.");
|
||||
refusedConnections[name] = count;
|
||||
}
|
||||
return { requests: value.requests.map(parseRequestEntry), refusedConnections, phase: value.phase, profile: value.profile };
|
||||
}
|
||||
|
||||
function parseStats(value: unknown): LinkStats {
|
||||
if (!isRecord(value)
|
||||
|| typeof value.requests !== "number"
|
||||
|| typeof value.faults !== "number"
|
||||
|| typeof value.refusedConnections !== "number"
|
||||
|| typeof value.phase !== "string"
|
||||
|| !isProfile(value.profile)) {
|
||||
throw new Error("Link stats response has an invalid shape.");
|
||||
}
|
||||
return {
|
||||
requests: value.requests,
|
||||
faults: value.faults,
|
||||
refusedConnections: value.refusedConnections,
|
||||
phase: value.phase,
|
||||
profile: value.profile,
|
||||
};
|
||||
}
|
||||
|
||||
async function adminJson(baseUrl: string, path: string, token: string, body?: object): Promise<unknown> {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
...(body === undefined ? {} : { "content-type": "application/json" }),
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`Den link admin ${path} failed with HTTP ${response.status}: ${text.slice(0, 1_000)}`);
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`Den link admin ${path} returned invalid JSON: ${text.slice(0, 1_000)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function distinctLocalPorts(): Promise<[number, number]> {
|
||||
const port = await allocateFreePort();
|
||||
let adminPort = await allocateFreePort();
|
||||
while (adminPort === port) adminPort = await allocateFreePort();
|
||||
return [port, adminPort];
|
||||
}
|
||||
|
||||
function waitForExit(child: ChildProcess): Promise<void> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
||||
return new Promise((resolve) => child.once("exit", () => resolve()));
|
||||
}
|
||||
|
||||
async function localPlacement(upstream: DenRef, token: string): Promise<LinkPlacement> {
|
||||
const [port, adminPort] = await distinctLocalPorts();
|
||||
const script = fileURLToPath(new URL("./link-server.mjs", import.meta.url));
|
||||
const child = spawn(process.execPath, [script, "--upstream", upstream.webUrl, "--port", String(port), "--admin-port", String(adminPort)], {
|
||||
env: { ...process.env, LINK_ADMIN_TOKEN: token },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (!child.stdout || !child.stderr) throw new Error("Den link child process did not expose output streams.");
|
||||
let output = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => { output += String(chunk); });
|
||||
child.stderr.on("data", (chunk) => { output += String(chunk); });
|
||||
const listening = `link-server listening data=${port} admin=${adminPort}`;
|
||||
try {
|
||||
const deadline = Date.now() + 15_000;
|
||||
while (!output.includes(listening) && Date.now() < deadline) {
|
||||
if (child.exitCode !== null) throw new Error(`Den link exited during startup (${child.exitCode}): ${output.slice(-2_000)}`);
|
||||
await delay(50);
|
||||
}
|
||||
if (!output.includes(listening)) throw new Error(`Timed out waiting for Den link startup: ${output.slice(-2_000)}`);
|
||||
await adminJson(`http://127.0.0.1:${adminPort}`, "/health", token);
|
||||
} catch (error) {
|
||||
child.kill("SIGKILL");
|
||||
throw error;
|
||||
}
|
||||
let disposed = false;
|
||||
return {
|
||||
dataUrl: `http://127.0.0.1:${port}`,
|
||||
adminUrl: `http://127.0.0.1:${adminPort}`,
|
||||
async dispose(): Promise<void> {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
child.kill("SIGTERM");
|
||||
const exited = waitForExit(child);
|
||||
await Promise.race([exited, delay(5_000)]);
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill("SIGKILL");
|
||||
await waitForExit(child);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function safeRemoteUrl(value: string): string {
|
||||
const parsed = new URL(value);
|
||||
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || !/^[A-Za-z0-9:/?&=._~%+\-]+$/.test(value)) {
|
||||
throw new Error(`Den link upstream URL is not safe for Daytona execution: ${value}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validPort(value: number, name: string): number {
|
||||
if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error(`${name} must be a valid port.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function remoteExec(exec: DaytonaExec, sandbox: string, script: string, context: string, timeoutMs = 30_000): Promise<string> {
|
||||
if (script.includes("'")) throw new Error(`Remote script for ${context} must not contain single quotes.`);
|
||||
const args = ["exec", sandbox, "--", `bash -lc '${script}'`];
|
||||
const commandLength = args.join(" ").length;
|
||||
if (commandLength > MAX_LINK_DAYTONA_COMMAND_LENGTH) {
|
||||
throw new Error(`Den link Daytona command is ${commandLength} characters; maximum is ${MAX_LINK_DAYTONA_COMMAND_LENGTH}.`);
|
||||
}
|
||||
const result = await checkedExec(exec, args, context, { timeoutMs });
|
||||
return `${result.stdout}${result.stderr}`;
|
||||
}
|
||||
|
||||
function firstHttpsUrl(text: string): string | null {
|
||||
const match = /https:\/\/[^\s"'<>)]+/.exec(text);
|
||||
return match ? match[0].replace(/[.,;:]+$/, "") : null;
|
||||
}
|
||||
|
||||
export function daytonaLinkCommands(
|
||||
source: Uint8Array,
|
||||
upstream: string,
|
||||
port: number,
|
||||
adminPort: number,
|
||||
token: string,
|
||||
): { cleanup: string; upload: string[]; detach: string } {
|
||||
if (source.byteLength === 0 || source.byteLength > MAX_LINK_SCRIPT_BYTES) {
|
||||
throw new Error(`Den link runner must be between 1 and ${MAX_LINK_SCRIPT_BYTES} bytes.`);
|
||||
}
|
||||
const upstreamUrl = safeRemoteUrl(upstream);
|
||||
const dataPort = validPort(port, "port");
|
||||
const controlPort = validPort(adminPort, "adminPort");
|
||||
if (!/^[a-f0-9]{32,}$/.test(token)) throw new Error("Den link admin token must be at least 32 lowercase hex characters.");
|
||||
const encoded = Buffer.from(source).toString("base64");
|
||||
const upload = [`: > ${DAYTONA_LINK_BASE64}`];
|
||||
for (let offset = 0; offset < encoded.length; offset += DAYTONA_LINK_BASE64_CHUNK_LENGTH) {
|
||||
const chunk = encoded.slice(offset, offset + DAYTONA_LINK_BASE64_CHUNK_LENGTH);
|
||||
upload.push(`printf %s ${chunk} >> ${DAYTONA_LINK_BASE64}`);
|
||||
}
|
||||
upload.push([
|
||||
"decode_status=0",
|
||||
`base64 -d ${DAYTONA_LINK_BASE64} > ${DAYTONA_LINK_SCRIPT} || decode_status=$?`,
|
||||
`actual_bytes=$(wc -c < ${DAYTONA_LINK_SCRIPT})`,
|
||||
`rm -f ${DAYTONA_LINK_BASE64}`,
|
||||
'test "$decode_status" -eq 0',
|
||||
`test "$actual_bytes" -eq ${source.byteLength}`,
|
||||
].join("; "));
|
||||
return {
|
||||
cleanup: `if test -f ${DAYTONA_LINK_PID}; then pid=$(<${DAYTONA_LINK_PID}); case "$pid" in ""|*[!0-9]*) ;; *) if test "$pid" -gt 0 2>/dev/null; then kill "$pid" 2>/dev/null || true; attempts=0; while kill -0 "$pid" 2>/dev/null && test "$attempts" -lt 50; do sleep 0.1; attempts=$((attempts + 1)); done; fi ;; esac; fi; rm -f ${DAYTONA_LINK_PID} ${DAYTONA_LINK_SCRIPT} ${DAYTONA_LINK_BASE64}`,
|
||||
upload,
|
||||
detach: `python3 - <<PYEOF
|
||||
import os
|
||||
import subprocess
|
||||
log = open("/tmp/link-server.log", "ab", buffering=0)
|
||||
process = subprocess.Popen(["node", "${DAYTONA_LINK_SCRIPT}", "--upstream", "${upstreamUrl}", "--port", "${dataPort}", "--admin-port", "${controlPort}"], stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, start_new_session=True, close_fds=True, env={**os.environ, "LINK_ADMIN_TOKEN": "${token}"})
|
||||
temporary_pid = "${DAYTONA_LINK_PID}." + str(os.getpid())
|
||||
try:
|
||||
with open(temporary_pid, "w", encoding="ascii") as pid_file:
|
||||
pid_file.write(str(process.pid) + "\\n")
|
||||
pid_file.flush()
|
||||
os.fsync(pid_file.fileno())
|
||||
os.replace(temporary_pid, "${DAYTONA_LINK_PID}")
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(temporary_pid)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
process.terminate()
|
||||
raise
|
||||
PYEOF`,
|
||||
};
|
||||
}
|
||||
|
||||
async function daytonaPlacement(upstream: DenRef, sandbox: string, options: DenLinkOptions, token: string): Promise<LinkPlacement> {
|
||||
const exec = options.daytonaExec ?? defaultDaytonaExec;
|
||||
const port = validPort(options.port ?? 3985, "port");
|
||||
const adminPort = validPort(options.adminPort ?? 3986, "adminPort");
|
||||
const client = options.client ?? "public-preview";
|
||||
if (port === adminPort) throw new Error("Den link data and admin ports must differ.");
|
||||
// Pin the original public Den URL before creating the shaper preview URLs;
|
||||
// running inside that Den sandbox therefore cannot proxy back into itself.
|
||||
const upstreamUrl = safeRemoteUrl(upstream.webUrl);
|
||||
const source = await readFile(fileURLToPath(new URL("./link-server.mjs", import.meta.url)));
|
||||
const commands = daytonaLinkCommands(source, upstreamUrl, port, adminPort, token);
|
||||
const cleanup = (): Promise<string> => remoteExec(exec, sandbox, commands.cleanup, `Den link cleanup for ${sandbox}`);
|
||||
await cleanup();
|
||||
try {
|
||||
for (const [index, upload] of commands.upload.entries()) {
|
||||
await remoteExec(exec, sandbox, upload, `Den link upload ${index + 1}/${commands.upload.length} for ${sandbox}`, 30_000);
|
||||
}
|
||||
await remoteExec(exec, sandbox, commands.detach, `Den link detach for ${sandbox}`);
|
||||
const deadline = Date.now() + 60_000;
|
||||
let last = "not attempted";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
await remoteExec(exec, sandbox, `curl -sf -H "Authorization: Bearer ${token}" http://127.0.0.1:${adminPort}/health`, `Den link health for ${sandbox}`, 10_000);
|
||||
last = "ok";
|
||||
break;
|
||||
} catch (error) {
|
||||
last = messageText(error);
|
||||
await delay(1_000);
|
||||
}
|
||||
}
|
||||
if (last !== "ok") {
|
||||
const log = await remoteExec(exec, sandbox, "tail -80 /tmp/link-server.log 2>&1 || true", `Den link log for ${sandbox}`).catch((error: unknown) => messageText(error));
|
||||
throw new Error(`Den link health gate failed in ${sandbox}. Last: ${last}. Log tail:\n${log.slice(-4_000)}`);
|
||||
}
|
||||
const dataPreview = client === "public-preview"
|
||||
? await checkedExec(exec, ["preview-url", sandbox, "-p", String(port)], `daytona preview-url ${sandbox} -p ${port}`, { timeoutMs: 30_000 })
|
||||
: null;
|
||||
const adminPreview = await checkedExec(exec, ["preview-url", sandbox, "-p", String(adminPort)], `daytona preview-url ${sandbox} -p ${adminPort}`, { timeoutMs: 30_000 });
|
||||
const dataUrl = dataPreview ? firstHttpsUrl(dataPreview.stdout) : `http://127.0.0.1:${port}`;
|
||||
const adminUrl = firstHttpsUrl(adminPreview.stdout);
|
||||
if (!dataUrl) throw new Error(`daytona preview-url ${sandbox} -p ${port} did not print an https URL: ${dataPreview?.stdout.trim()}`);
|
||||
if (!adminUrl) throw new Error(`daytona preview-url ${sandbox} -p ${adminPort} did not print an https URL: ${adminPreview.stdout.trim()}`);
|
||||
await adminJson(adminUrl, "/health", token);
|
||||
let disposed = false;
|
||||
return {
|
||||
dataUrl,
|
||||
adminUrl,
|
||||
async dispose(): Promise<void> {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
await cleanup().catch((error: unknown) => console.error(`Den link cleanup failed for ${sandbox}: ${messageText(error)}`));
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
await cleanup().catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function denLink(upstream: DenRef, options: DenLinkOptions = {}): Promise<DenLink> {
|
||||
const token = randomBytes(32).toString("hex");
|
||||
const placement = options.sandboxId
|
||||
? await daytonaPlacement(upstream, options.sandboxId, options, token)
|
||||
: await localPlacement(upstream, token);
|
||||
const post = async (path: string, body: object): Promise<void> => {
|
||||
await adminJson(placement.adminUrl, path, token, body);
|
||||
};
|
||||
return {
|
||||
ref: { apiUrl: `${placement.dataUrl}/api/den`, webUrl: placement.dataUrl },
|
||||
admin: {
|
||||
phase: (name, profile) => post("/phase", profile === undefined ? { name } : { name, profile }),
|
||||
rules: (newRules) => post("/rules", { rules: newRules }),
|
||||
bandwidth: (rate) => post("/bandwidth", { bytesPerSec: rate }),
|
||||
offline: (durationMs) => post("/offline", { durationMs }),
|
||||
clear: () => post("/clear", {}),
|
||||
async requests(): Promise<LinkLog> {
|
||||
return parseLog(await adminJson(placement.adminUrl, "/requests", token));
|
||||
},
|
||||
async stats(): Promise<LinkStats> {
|
||||
return parseStats(await adminJson(placement.adminUrl, "/stats", token));
|
||||
},
|
||||
async health(): Promise<{ ok: boolean; phase: string; offline: boolean }> {
|
||||
return parseHealth(await adminJson(placement.adminUrl, "/health", token));
|
||||
},
|
||||
},
|
||||
[Symbol.asyncDispose]: () => placement.dispose(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { closeSync, openSync } from "node:fs";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { allocateFreePorts } from "@openwork/cdp";
|
||||
import { SkipError, ephemeralDatabaseName, localMysqlIsRunning, localRedisIsRunning, trustedOrigins } from "@openwork/env";
|
||||
import { freePort, killLocalPid } from "@openwork/hosts";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import type { DenRef } from "@openwork/behaviors";
|
||||
import type { DbHandle, Place } from "@openwork/env";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const REPO_ROOT = fileURLToPath(new URL("../../../..", import.meta.url));
|
||||
const DATABASE_ENCRYPTION_KEY = "local-dev-db-encryption-key-please-change-1234567890";
|
||||
const BETTER_AUTH_SECRET = "local-testkit-secret-not-for-production-use!!";
|
||||
const START_TIMEOUT_MS = 120_000;
|
||||
|
||||
export interface SelfHostServerOptions {
|
||||
place: Place;
|
||||
name: string;
|
||||
slug: string;
|
||||
ownerEmails: string[];
|
||||
allowPublicSignup?: boolean;
|
||||
}
|
||||
|
||||
export interface SelfHostDen {
|
||||
ref: DenRef;
|
||||
database: string;
|
||||
}
|
||||
|
||||
interface SpawnedService {
|
||||
child: ChildProcess;
|
||||
pid: number;
|
||||
port: number;
|
||||
label: string;
|
||||
logPath: string;
|
||||
}
|
||||
|
||||
function messageText(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function cleanUrl(value: string): string {
|
||||
let out = value.trim();
|
||||
while (out.endsWith("/")) out = out.slice(0, -1);
|
||||
return out;
|
||||
}
|
||||
|
||||
// mirrored from server.ts (keep in sync)
|
||||
function spawnService(
|
||||
label: string,
|
||||
script: "dev:den:api" | "dev:den:web",
|
||||
port: number,
|
||||
env: NodeJS.ProcessEnv,
|
||||
logPath: string,
|
||||
): SpawnedService {
|
||||
const logFd = openSync(logPath, "a");
|
||||
const prepared = process.env.OPENWORK_EVAL_DEN_RUNTIME_PREPARED === "1";
|
||||
const args = prepared
|
||||
? label === "den-api"
|
||||
? ["--filter", "@openwork-ee/den-api", "exec", "tsx", "src/main.ts"]
|
||||
: ["--filter", "@openwork-ee/den-web", "exec", "next", "start", "--hostname", "127.0.0.1", "--port", String(port)]
|
||||
: [script];
|
||||
const child = spawn("pnpm", args, {
|
||||
cwd: REPO_ROOT,
|
||||
env: prepared && label === "den-api" ? { ...env, PORT: String(port) } : env,
|
||||
detached: true,
|
||||
stdio: ["ignore", logFd, logFd],
|
||||
});
|
||||
closeSync(logFd);
|
||||
child.unref();
|
||||
if (!child.pid) throw new Error(`Could not start ${label}.`);
|
||||
return { child, pid: child.pid, port, label, logPath };
|
||||
}
|
||||
|
||||
// mirrored from server.ts (keep in sync)
|
||||
async function logTail(path: string): Promise<string> {
|
||||
return readFile(path, "utf8")
|
||||
.then((text) => text.split(/\r?\n/).slice(-40).join("\n"))
|
||||
.catch((error: unknown) => `log unavailable: ${messageText(error)}`);
|
||||
}
|
||||
|
||||
// mirrored from server.ts (keep in sync)
|
||||
async function waitForHttp(url: string, service: SpawnedService, accept: (response: Response) => boolean): Promise<Response> {
|
||||
const deadline = Date.now() + START_TIMEOUT_MS;
|
||||
let last = "not attempted";
|
||||
while (Date.now() < deadline) {
|
||||
if (service.child.exitCode !== null) {
|
||||
throw new Error(`${service.label} exited with ${service.child.exitCode}. Last log lines:\n${await logTail(service.logPath)}`);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(5_000) });
|
||||
if (accept(response)) return response;
|
||||
last = `HTTP ${response.status}`;
|
||||
} catch (error) {
|
||||
last = messageText(error);
|
||||
}
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${service.label} at ${url}: ${last}. Last log lines:\n${await logTail(service.logPath)}`);
|
||||
}
|
||||
|
||||
// mirrored from server.ts (keep in sync)
|
||||
async function waitForAuthProbe(ref: DenRef, service: SpawnedService): Promise<void> {
|
||||
const url = `${cleanUrl(ref.apiUrl)}/api/auth/sign-in/email`;
|
||||
const deadline = Date.now() + START_TIMEOUT_MS;
|
||||
let last = "not attempted";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", origin: ref.webUrl },
|
||||
body: JSON.stringify({ email: `probe-${Date.now()}@openwork.test`, password: "not-a-real-password" }),
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
});
|
||||
if (response.status !== 403 && response.status < 500) return;
|
||||
last = `HTTP ${response.status} ${(await response.text()).slice(0, 300)}`;
|
||||
} catch (error) {
|
||||
last = messageText(error);
|
||||
}
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error(`Den auth behavioral probe failed at ${url}; expected a non-403 response. Last: ${last}. Log:\n${await logTail(service.logPath)}`);
|
||||
}
|
||||
|
||||
// mirrored from server.ts (keep in sync)
|
||||
async function runDbPush(databaseUrl: string): Promise<void> {
|
||||
try {
|
||||
const commands = process.env.OPENWORK_EVAL_DEN_RUNTIME_PREPARED === "1"
|
||||
? [
|
||||
["--filter", "@openwork-ee/den-db", "exec", "node", "--import", "tsx", "./node_modules/drizzle-kit/bin.cjs", "push", "--config", "drizzle.config.ts"],
|
||||
["--filter", "@openwork-ee/den-db", "exec", "node", "--import", "tsx", "scripts/ensure-schema-repairs.ts"],
|
||||
]
|
||||
: [["--filter", "@openwork-ee/den-db", "db:push"]];
|
||||
for (const args of commands) {
|
||||
await execFileAsync("pnpm", args, {
|
||||
cwd: REPO_ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: databaseUrl,
|
||||
DEN_DB_ENCRYPTION_KEY: DATABASE_ENCRYPTION_KEY,
|
||||
},
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
timeout: 180_000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const stderr = typeof error === "object" && error !== null && typeof Reflect.get(error, "stderr") === "string"
|
||||
? Reflect.get(error, "stderr")
|
||||
: "";
|
||||
throw new Error(`Ephemeral Den database push failed: ${messageText(error)}${stderr ? `\n${stderr}` : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
// mirrored from server.ts (keep in sync)
|
||||
async function stopServices(services: SpawnedService[]): Promise<void> {
|
||||
for (const service of services) {
|
||||
await killLocalPid(service.pid, { log: (line) => console.error(`[openwork/testkit] ${line}`) })
|
||||
.catch((error: unknown) => console.error(`[openwork/testkit] ${service.label} cleanup failed: ${messageText(error)}`));
|
||||
await freePort(service.port)
|
||||
.catch((error: unknown) => console.error(`[openwork/testkit] ${service.label} port cleanup failed: ${messageText(error)}`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function selfHostServer(options: SelfHostServerOptions): Promise<SelfHostDen & AsyncDisposable> {
|
||||
if (options.place.kind === "daytona") {
|
||||
throw new SkipError("selfHostServer requires local placement; unset OPENWORK_EVAL_DAYTONA");
|
||||
}
|
||||
if (!await localMysqlIsRunning()) {
|
||||
throw new Error("Local Den requires MySQL on 127.0.0.1:3306. Run: pnpm dev:den:mysql");
|
||||
}
|
||||
if (!await localRedisIsRunning()) {
|
||||
throw new Error("Local Den requires Redis on 127.0.0.1:6379. Run: redis-server --port 6379 --daemonize yes --save '' --appendonly no");
|
||||
}
|
||||
|
||||
const services: SpawnedService[] = [];
|
||||
let database: DbHandle | undefined;
|
||||
try {
|
||||
database = await options.place.db(ephemeralDatabaseName("openwork_selfhost_eval"));
|
||||
await runDbPush(database.url);
|
||||
const [apiPort, webPort] = await allocateFreePorts(2);
|
||||
if (apiPort === undefined || webPort === undefined) throw new Error("Could not allocate Den API/Web ports.");
|
||||
|
||||
const origins = trustedOrigins(apiPort, webPort).join(",");
|
||||
const ref: DenRef = {
|
||||
apiUrl: `http://127.0.0.1:${apiPort}`,
|
||||
webUrl: `http://127.0.0.1:${webPort}`,
|
||||
};
|
||||
const logsDir = join(REPO_ROOT, "evals", "results", ".testkit", database.name);
|
||||
await mkdir(logsDir, { recursive: true });
|
||||
const commonEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
DATABASE_URL: database.url,
|
||||
DEN_DB_ENCRYPTION_KEY: DATABASE_ENCRYPTION_KEY,
|
||||
BETTER_AUTH_SECRET,
|
||||
BETTER_AUTH_URL: `http://localhost:${webPort}`,
|
||||
DEN_BASE_URL: `http://localhost:${webPort}`,
|
||||
DEN_API_PUBLIC_URL: ref.apiUrl,
|
||||
DEN_API_PORT: String(apiPort),
|
||||
DEN_WEB_PORT: String(webPort),
|
||||
DEN_BETTER_AUTH_TRUSTED_ORIGINS: origins,
|
||||
CORS_ORIGINS: origins,
|
||||
DEN_ORG_MODE: "single_org",
|
||||
DEN_SINGLE_ORG_NAME: options.name,
|
||||
DEN_SINGLE_ORG_SLUG: options.slug,
|
||||
DEN_SINGLE_ORG_OWNER_EMAILS: options.ownerEmails.join(","),
|
||||
DEN_SINGLE_ORG_ALLOW_PUBLIC_SIGNUP: String(options.allowPublicSignup ?? false),
|
||||
DEN_REQUIRE_EMAIL_VERIFICATION: "false",
|
||||
DEN_PASSWORD_BREACH_SCREENING_ENABLED: "false",
|
||||
OPENWORK_DEV_MODE: "1",
|
||||
PROVISIONER_MODE: "stub",
|
||||
};
|
||||
|
||||
const api = spawnService("den-api", "dev:den:api", apiPort, {
|
||||
...commonEnv,
|
||||
DEN_BIND_HOST: "127.0.0.1",
|
||||
}, join(logsDir, "api.log"));
|
||||
services.push(api);
|
||||
const web = spawnService("den-web", "dev:den:web", webPort, {
|
||||
DEN_WEB_HOST: "127.0.0.1",
|
||||
...commonEnv,
|
||||
DEN_API_BASE: `http://127.0.0.1:${apiPort}`,
|
||||
DEN_BASE_URL: `http://localhost:${webPort}`,
|
||||
DEN_AUTH_ORIGIN: `http://localhost:${webPort}`,
|
||||
DEN_AUTH_FALLBACK_BASE: `http://127.0.0.1:${apiPort}`,
|
||||
}, join(logsDir, "web.log"));
|
||||
services.push(web);
|
||||
|
||||
await waitForHttp(`${ref.apiUrl}/health`, api, (response) => response.ok);
|
||||
await waitForHttp(`${ref.webUrl}/api/ready`, web, (response) => response.ok);
|
||||
await waitForAuthProbe(ref, api);
|
||||
|
||||
let disposed = false;
|
||||
return {
|
||||
ref,
|
||||
database: database.name,
|
||||
async [Symbol.asyncDispose](): Promise<void> {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
await stopServices(services);
|
||||
await database?.drop().catch((error: unknown) => {
|
||||
console.error(`[openwork/testkit] ephemeral database cleanup failed: ${messageText(error)}`);
|
||||
});
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
await stopServices(services);
|
||||
await database?.drop().catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "@openwork/behaviors";
|
||||
export * from "@openwork/hosts";
|
||||
export * from "@openwork/env";
|
||||
export * from "./eventually.ts";
|
||||
export * from "./link.ts";
|
||||
export * from "./self-host.ts";
|
||||
export * from "./state.ts";
|
||||
@@ -0,0 +1,263 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join, posix } from "node:path";
|
||||
import { evalIn } from "@openwork/behaviors";
|
||||
import { defaultDaytonaExec, electronProfilePaths, execInSandbox } from "@openwork/hosts";
|
||||
import type { Surface } from "@openwork/cdp";
|
||||
|
||||
export interface DenClientState {
|
||||
authTokenPresent: boolean;
|
||||
activeOrgId: string | null;
|
||||
activeOrgSlug: string | null;
|
||||
activeOrgName: string | null;
|
||||
}
|
||||
|
||||
export interface ConnectState {
|
||||
ok: boolean;
|
||||
status: "available" | "missing" | "invalid" | "unreadable" | null;
|
||||
connectEnabled: boolean | null;
|
||||
raw: unknown;
|
||||
}
|
||||
|
||||
export interface CloudMcpHealthSummary {
|
||||
ok: boolean;
|
||||
phase: string | null;
|
||||
usable: boolean | null;
|
||||
engineStatus: string | null;
|
||||
tools: { present: string[]; missing: string[] };
|
||||
direct: { checked: boolean; source: string | null; present: string[]; missing: string[] };
|
||||
raw: unknown;
|
||||
}
|
||||
|
||||
export interface ConnectStateFile {
|
||||
status: "missing" | "available" | "invalid";
|
||||
connectEnabled: boolean | null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function nullableString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function stringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
function errorCode(error: unknown): string | null {
|
||||
return isRecord(error) && typeof error.code === "string" ? error.code : null;
|
||||
}
|
||||
|
||||
export async function readDenClientState(app: Surface): Promise<DenClientState> {
|
||||
const value = await evalIn(app, `(() => ({
|
||||
authTokenPresent: Boolean((localStorage.getItem("openwork.den.authToken") ?? "").trim()),
|
||||
activeOrgId: (localStorage.getItem("openwork.den.activeOrgId") ?? "").trim() || null,
|
||||
activeOrgSlug: (localStorage.getItem("openwork.den.activeOrgSlug") ?? "").trim() || null,
|
||||
activeOrgName: (localStorage.getItem("openwork.den.activeOrgName") ?? "").trim() || null,
|
||||
}))()`);
|
||||
if (!isRecord(value)) throw new Error("The desktop returned an invalid Den client state.");
|
||||
return {
|
||||
authTokenPresent: value.authTokenPresent === true,
|
||||
activeOrgId: nullableString(value.activeOrgId),
|
||||
activeOrgSlug: nullableString(value.activeOrgSlug),
|
||||
activeOrgName: nullableString(value.activeOrgName),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readConnectState(app: Surface): Promise<ConnectState> {
|
||||
const value = await evalIn(app, `(async () => {
|
||||
// Resolve the local server the same way the app does: live runtime info
|
||||
// from the Electron bridge first (loopback port + token are ephemeral per
|
||||
// boot), then the web-mode localStorage overrides as a fallback.
|
||||
let baseUrl = "";
|
||||
let token = "";
|
||||
try {
|
||||
const invokeDesktop = window.__OPENWORK_ELECTRON__ && window.__OPENWORK_ELECTRON__.invokeDesktop;
|
||||
if (invokeDesktop) {
|
||||
const info = await invokeDesktop("openworkServerInfo");
|
||||
if (info && info.running === true) {
|
||||
baseUrl = String(info.baseUrl ?? info.connectUrl ?? "").trim().replace(/\\/+$/, "");
|
||||
token = String(info.ownerToken ?? info.clientToken ?? "").trim();
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
if (!baseUrl || !token) {
|
||||
const port = (localStorage.getItem("openwork.server.port") ?? "").trim();
|
||||
baseUrl = port ? "http://127.0.0.1:" + port : baseUrl;
|
||||
token = token || (localStorage.getItem("openwork.server.token") ?? "").trim();
|
||||
}
|
||||
if (!baseUrl || !token) {
|
||||
return { ok: false, status: null, connectEnabled: null, raw: { error: "Local server credentials are unavailable." } };
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
baseUrl + "/experimental/connect/state",
|
||||
{ headers: { Authorization: "Bearer " + token } },
|
||||
);
|
||||
const text = await response.text();
|
||||
let raw = text;
|
||||
try { raw = text ? JSON.parse(text) : null; } catch {}
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: raw && typeof raw === "object"
|
||||
&& (raw.status === "available" || raw.status === "missing" || raw.status === "invalid" || raw.status === "unreadable")
|
||||
? raw.status
|
||||
: null,
|
||||
connectEnabled: raw && typeof raw === "object" && typeof raw.connectEnabled === "boolean"
|
||||
? raw.connectEnabled
|
||||
: null,
|
||||
raw,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: null,
|
||||
connectEnabled: null,
|
||||
raw: { error: error instanceof Error ? error.message : String(error) },
|
||||
};
|
||||
}
|
||||
})()`, { awaitPromise: true, timeoutMs: 15_000 });
|
||||
if (!isRecord(value)) throw new Error("The desktop returned an invalid Connect state.");
|
||||
return {
|
||||
ok: value.ok === true,
|
||||
status: value.status === "available" || value.status === "missing" || value.status === "invalid" || value.status === "unreadable"
|
||||
? value.status
|
||||
: null,
|
||||
connectEnabled: typeof value.connectEnabled === "boolean" ? value.connectEnabled : null,
|
||||
raw: value.raw,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readCloudMcpHealth(
|
||||
app: Surface,
|
||||
workspaceId: string,
|
||||
opts?: { probe?: boolean; timeoutMs?: number },
|
||||
): Promise<CloudMcpHealthSummary> {
|
||||
const value = await evalIn(app, `(async () => {
|
||||
// Resolve the local server the same way the app does: live runtime info
|
||||
// from the Electron bridge first (loopback port + token are ephemeral per
|
||||
// boot), then the web-mode localStorage overrides as a fallback.
|
||||
let baseUrl = "";
|
||||
let token = "";
|
||||
try {
|
||||
const invokeDesktop = window.__OPENWORK_ELECTRON__ && window.__OPENWORK_ELECTRON__.invokeDesktop;
|
||||
if (invokeDesktop) {
|
||||
const info = await invokeDesktop("openworkServerInfo");
|
||||
if (info && info.running === true) {
|
||||
baseUrl = String(info.baseUrl ?? info.connectUrl ?? "").trim().replace(/\\/+$/, "");
|
||||
token = String(info.ownerToken ?? info.clientToken ?? "").trim();
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
if (!baseUrl || !token) {
|
||||
const port = (localStorage.getItem("openwork.server.port") ?? "").trim();
|
||||
baseUrl = port ? "http://127.0.0.1:" + port : baseUrl;
|
||||
token = token || (localStorage.getItem("openwork.server.token") ?? "").trim();
|
||||
}
|
||||
if (!baseUrl || !token) {
|
||||
return { ok: false, raw: { error: "Local server credentials are unavailable." } };
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
baseUrl + "/workspace/" + encodeURIComponent(${JSON.stringify(workspaceId)}) + "/mcp/openwork-cloud/health" + ${JSON.stringify(opts?.probe === true ? "?probe=1" : "")},
|
||||
{ headers: { Authorization: "Bearer " + token } },
|
||||
);
|
||||
const text = await response.text();
|
||||
let raw = text;
|
||||
try { raw = text ? JSON.parse(text) : null; } catch {}
|
||||
return { ok: response.ok, raw };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
raw: { error: error instanceof Error ? error.message : String(error) },
|
||||
};
|
||||
}
|
||||
})()`, { awaitPromise: true, timeoutMs: opts?.timeoutMs ?? 15_000 });
|
||||
const result = isRecord(value) ? value : {};
|
||||
const raw = result.raw;
|
||||
const health = isRecord(raw) ? raw : {};
|
||||
const engine = isRecord(health.engine) ? health.engine : {};
|
||||
const tools = isRecord(health.tools) ? health.tools : {};
|
||||
const direct = isRecord(tools.direct) ? tools.direct : {};
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
phase: nullableString(health.phase),
|
||||
usable: typeof health.usable === "boolean" ? health.usable : null,
|
||||
engineStatus: nullableString(engine.status),
|
||||
tools: {
|
||||
present: stringArray(tools.present),
|
||||
missing: stringArray(tools.missing),
|
||||
},
|
||||
direct: {
|
||||
checked: direct.checked === true,
|
||||
source: nullableString(direct.source),
|
||||
present: stringArray(direct.present),
|
||||
missing: stringArray(direct.missing),
|
||||
},
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reads the caller-visible profile filesystem. */
|
||||
export async function readConnectStateFile(
|
||||
app: Surface,
|
||||
deps?: { exec?: (sandbox: string, script: string) => Promise<string> },
|
||||
): Promise<ConnectStateFile> {
|
||||
if (app.handle.hostKind !== "local" && app.handle.hostKind !== "daytona") {
|
||||
throw new Error(`readConnectStateFile supports local and daytona app profiles; received ${app.handle.hostKind}.`);
|
||||
}
|
||||
if (!app.handle.profileDir) throw new Error(`The ${app.handle.hostKind} app did not expose its profile directory.`);
|
||||
// The local server persists runtime state next to its config file
|
||||
// (`openworkConfigDir()`). The dev-mode desktop redirects that XDG config
|
||||
// root under its Electron userData dir (`<userData>/openwork-dev-data/xdg/config`),
|
||||
// so probe the known layouts in order.
|
||||
const paths = electronProfilePaths(app.handle.profileDir);
|
||||
const pathJoin = app.handle.hostKind === "daytona" ? posix.join : join;
|
||||
const candidates = [
|
||||
pathJoin(paths.userDataDir, "openwork-dev-data", "xdg", "config", "openwork", "connect-state.json"),
|
||||
pathJoin(paths.configHome, "openwork", "connect-state.json"),
|
||||
pathJoin(paths.homeDir, ".config", "openwork", "connect-state.json"),
|
||||
];
|
||||
let text: string | null = null;
|
||||
if (app.handle.hostKind === "daytona") {
|
||||
if (!app.handle.sandboxId) throw new Error("The daytona app did not expose its sandbox ID.");
|
||||
const sandbox = app.handle.sandboxId;
|
||||
const exec = deps?.exec ?? (async (sandboxId: string, script: string): Promise<string> => {
|
||||
const result = await execInSandbox(defaultDaytonaExec, sandboxId, script, {
|
||||
timeoutMs: 30_000,
|
||||
context: `connect-state read for ${sandboxId}`,
|
||||
});
|
||||
return result.stdout;
|
||||
});
|
||||
for (const path of candidates) {
|
||||
if (!/^\/[A-Za-z0-9._/-]+$/.test(path)) {
|
||||
throw new Error(`Unsafe connect-state path ${JSON.stringify(path)}: only absolute paths containing letters, digits and . _ / - are allowed.`);
|
||||
}
|
||||
const output = await exec(sandbox, `if [ -f "${path}" ]; then cat "${path}"; else echo __OPENWORK_TESTKIT_MISSING__; fi`);
|
||||
if (output.trim() !== "__OPENWORK_TESTKIT_MISSING__") {
|
||||
text = output;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const path of candidates) {
|
||||
try {
|
||||
text = await readFile(path, "utf8");
|
||||
break;
|
||||
} catch (error) {
|
||||
if (errorCode(error) !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text === null) return { status: "missing", connectEnabled: null };
|
||||
try {
|
||||
const value: unknown = JSON.parse(text);
|
||||
if (!isRecord(value) || typeof value.connectEnabled !== "boolean") {
|
||||
return { status: "invalid", connectEnabled: null };
|
||||
}
|
||||
return { status: "available", connectEnabled: value.connectEnabled };
|
||||
} catch {
|
||||
return { status: "invalid", connectEnabled: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { claim, createBriefRun, testBrief } from "../src/brief.ts";
|
||||
|
||||
test("testBrief rejects invalid definitions", () => {
|
||||
assert.throws(() => testBrief({ behavior: " ", claims: { valid: claim("works") } }), /behavior must not be empty/);
|
||||
assert.throws(() => testBrief({ behavior: "Works.", claims: {} }), /at least one claim/);
|
||||
assert.throws(() => testBrief({ behavior: "Works.", claims: { blankMust: claim(" ") } }), /blankMust.*blank must/);
|
||||
assert.throws(() => testBrief({ behavior: "Works.", claims: { blankNever: claim("works", { never: " " }) } }), /blankNever.*blank never/);
|
||||
});
|
||||
|
||||
test("a run records true proofs and reports its facts", () => {
|
||||
const recorded: { claimText: string; evidence: string; passed: boolean }[] = [];
|
||||
const brief = testBrief({
|
||||
behavior: "Two claims work.",
|
||||
claims: {
|
||||
first: claim("the first succeeds"),
|
||||
second: claim("the second succeeds", { never: "silently disappear" }),
|
||||
},
|
||||
});
|
||||
const run = createBriefRun(brief, (claimText, evidence, passed) => recorded.push({ claimText, evidence, passed }));
|
||||
|
||||
run.prove.first(true, "first observed");
|
||||
run.prove.second(true, "second observed");
|
||||
|
||||
assert.deepEqual(run.facts(), [
|
||||
{ key: "first", passed: true, evidence: "first observed" },
|
||||
{ key: "second", passed: true, evidence: "second observed" },
|
||||
]);
|
||||
assert.doesNotThrow(() => run.assertAllProven());
|
||||
assert.equal(recorded[0].claimText, "first: the first succeeds");
|
||||
assert.equal(recorded[1].claimText, "second: the second succeeds — never: silently disappear");
|
||||
});
|
||||
|
||||
test("a failed proof records before throwing", () => {
|
||||
const recorded: { passed: boolean }[] = [];
|
||||
const run = createBriefRun(testBrief({ behavior: "Failure is visible.", claims: { failure: claim("must pass") } }), (_claimText, _evidence, passed) => recorded.push({ passed }));
|
||||
|
||||
assert.throws(() => run.prove.failure(false, "observed failure"), /Claim failed: failure/);
|
||||
assert.deepEqual(recorded, [{ passed: false }]);
|
||||
});
|
||||
|
||||
test("blank proof evidence throws without recording", () => {
|
||||
const recorded: string[] = [];
|
||||
const run = createBriefRun(testBrief({ behavior: "Evidence is required.", claims: { evidenced: claim("has evidence") } }), (claimText) => recorded.push(claimText));
|
||||
|
||||
assert.throws(() => run.prove.evidenced(true, " "), /must not be blank/);
|
||||
assert.deepEqual(recorded, []);
|
||||
});
|
||||
|
||||
test("assertAllProven names only the missing claim", () => {
|
||||
const run = createBriefRun(testBrief({
|
||||
behavior: "Every claim is required.",
|
||||
claims: { proven: claim("is proven"), missing: claim("is also proven") },
|
||||
}), () => {});
|
||||
run.prove.proven(true, "proven here");
|
||||
|
||||
assert.throws(
|
||||
() => run.assertAllProven(),
|
||||
/^Error: Brief claims left unproven: missing\. Every declared claim needs a prove\.<claim>\(\) call\.$/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { eventually, sleep } from "../src/eventually.ts";
|
||||
|
||||
test("eventually polls until the default truthy condition passes", async () => {
|
||||
let attempts = 0;
|
||||
const value = await eventually(() => {
|
||||
attempts += 1;
|
||||
return attempts >= 3 ? "ready" : "";
|
||||
}, { within: 100, intervalMs: 1 });
|
||||
|
||||
assert.equal(value, "ready");
|
||||
assert.equal(attempts, 3);
|
||||
});
|
||||
|
||||
test("eventually supports custom conditions and asynchronous probes", async () => {
|
||||
let value = 0;
|
||||
const result = await eventually(async () => {
|
||||
await sleep(1);
|
||||
value += 1;
|
||||
return value;
|
||||
}, { within: 100, intervalMs: 1, until: (candidate) => candidate === 2 });
|
||||
|
||||
assert.equal(result, 2);
|
||||
});
|
||||
|
||||
test("eventually reports its label and last value on timeout", async () => {
|
||||
await assert.rejects(
|
||||
eventually(() => ({ ready: false }), {
|
||||
within: 5,
|
||||
intervalMs: 1,
|
||||
label: "fake readiness",
|
||||
until: (value) => value.ready,
|
||||
}),
|
||||
/Timed out waiting for fake readiness.*last value: \{"ready":false\}/,
|
||||
);
|
||||
});
|
||||
|
||||
test("eventually reports the last probe error on timeout", async () => {
|
||||
await assert.rejects(
|
||||
eventually(() => {
|
||||
throw new Error("fake failure");
|
||||
}, { within: 5, intervalMs: 1, label: "erroring probe" }),
|
||||
/Timed out waiting for erroring probe.*last error: "fake failure"/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,356 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer, request as httpRequest } from "node:http";
|
||||
import test from "node:test";
|
||||
import { daytonaLinkCommands, denLink, MAX_LINK_DAYTONA_COMMAND_LENGTH } from "../src/link.ts";
|
||||
import type { Server } from "node:http";
|
||||
import type { DaytonaExec } from "@openwork/hosts";
|
||||
|
||||
const LARGE_BODY = Buffer.alloc(192 * 1024, 97);
|
||||
const ADMIN_TOKEN = "a".repeat(64);
|
||||
|
||||
function listen(server: Server): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (typeof address === "object" && address !== null) resolve(address.port);
|
||||
else reject(new Error("Upstream test server did not expose a port."));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function close(server: Server): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.close((error) => error ? reject(error) : resolve());
|
||||
server.closeAllConnections();
|
||||
});
|
||||
}
|
||||
|
||||
function timedFetch(url: string, timeoutMs = 5_000): Promise<Response> {
|
||||
return fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
}
|
||||
|
||||
function absoluteGet(proxyUrl: string, target: string): Promise<string> {
|
||||
const proxy = new URL(proxyUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = httpRequest({ hostname: proxy.hostname, port: proxy.port, path: target }, (response) => {
|
||||
response.setEncoding("utf8");
|
||||
let body = "";
|
||||
response.on("data", (chunk: string) => body += chunk);
|
||||
response.on("end", () => resolve(body));
|
||||
});
|
||||
request.on("error", reject);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
test("Daytona link commands upload the runner-side script before launching its temp path", () => {
|
||||
const source = Buffer.alloc(32 * 1024, 97);
|
||||
const commands = daytonaLinkCommands(source, "https://den.example.test", 3985, 3986, ADMIN_TOKEN);
|
||||
assert(commands.cleanup.includes("pid=$(</tmp/openwork-den-link-server.pid)"));
|
||||
assert(commands.cleanup.includes('kill "$pid"'));
|
||||
assert(commands.cleanup.includes("rm -f /tmp/openwork-den-link-server.pid /tmp/openwork-den-link-server.mjs /tmp/openwork-den-link-server.mjs.b64"));
|
||||
assert(!commands.cleanup.includes("pkill"));
|
||||
assert.equal(commands.upload[0], ": > /tmp/openwork-den-link-server.mjs.b64");
|
||||
const chunkPattern = /^printf %s ([A-Za-z0-9+/=]+) >> \/tmp\/openwork-den-link-server\.mjs\.b64$/;
|
||||
const chunks = commands.upload.slice(1, -1).map((command) => {
|
||||
const match = chunkPattern.exec(command);
|
||||
assert(match);
|
||||
assert(match[1].length <= 8 * 1024);
|
||||
return match[1];
|
||||
});
|
||||
assert.equal(chunks.join(""), source.toString("base64"));
|
||||
const finalize = commands.upload.at(-1);
|
||||
assert(finalize?.includes("base64 -d /tmp/openwork-den-link-server.mjs.b64 > /tmp/openwork-den-link-server.mjs"));
|
||||
assert(finalize?.includes("actual_bytes=$(wc -c < /tmp/openwork-den-link-server.mjs)"));
|
||||
assert(finalize?.includes("rm -f /tmp/openwork-den-link-server.mjs.b64"));
|
||||
assert(finalize?.includes(`test \"$actual_bytes\" -eq ${source.byteLength}`));
|
||||
const commandLengths = [commands.cleanup, ...commands.upload, commands.detach]
|
||||
.map((command) => ["exec", "desktop-sandbox", "--", `bash -lc '${command}'`].join(" ").length);
|
||||
assert(Math.max(...commandLengths) <= MAX_LINK_DAYTONA_COMMAND_LENGTH);
|
||||
assert(commands.detach.includes('"node", "/tmp/openwork-den-link-server.mjs"'));
|
||||
assert(commands.detach.includes("pid_file.write(str(process.pid)"));
|
||||
assert(commands.detach.includes('os.replace(temporary_pid, "/tmp/openwork-den-link-server.pid")'));
|
||||
assert(commands.detach.includes(`env={**os.environ, "LINK_ADMIN_TOKEN": "${ADMIN_TOKEN}"}`));
|
||||
const launch = commands.detach.split("\n").find((line) => line.startsWith("process = subprocess.Popen"));
|
||||
assert(launch);
|
||||
assert(!launch.slice(0, launch.indexOf("], stdin=")).includes(ADMIN_TOKEN));
|
||||
assert(!commands.detach.includes("/workspace/evals"));
|
||||
assert.throws(
|
||||
() => daytonaLinkCommands(source, "https://den.example.test", 3985, 3986, "not-hex"),
|
||||
/at least 32 lowercase hex characters/,
|
||||
);
|
||||
});
|
||||
|
||||
test("denLink separates a sandbox loopback client from public admin control and defaults to public previews", async () => {
|
||||
const calls: string[][] = [];
|
||||
const exec: DaytonaExec = async (args) => {
|
||||
calls.push(args);
|
||||
const previewPort = args[0] === "preview-url" ? args.at(-1) : undefined;
|
||||
return {
|
||||
stdout: previewPort === "3985"
|
||||
? "https://data-preview.example.test\n"
|
||||
: previewPort === "3986" ? "https://admin-preview.example.test\n" : "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
};
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetched: Array<{ url: string; authorization: string | null }> = [];
|
||||
globalThis.fetch = async (input, init) => {
|
||||
fetched.push({ url: String(input), authorization: new Headers(init?.headers).get("authorization") });
|
||||
return new Response(JSON.stringify({ ok: true, phase: "default", offline: false }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
try {
|
||||
{
|
||||
await using link = await denLink(
|
||||
{ apiUrl: "https://den.example.test/api/den", webUrl: "https://den.example.test" },
|
||||
{ sandboxId: "desktop-sandbox", client: "sandbox-loopback", daytonaExec: exec },
|
||||
);
|
||||
assert.deepEqual(link.ref, {
|
||||
apiUrl: "http://127.0.0.1:3985/api/den",
|
||||
webUrl: "http://127.0.0.1:3985",
|
||||
});
|
||||
assert(calls.some((args) => args[0] === "exec" && args.at(-1)?.includes("http://127.0.0.1:3986/health")));
|
||||
assert.deepEqual(
|
||||
calls.filter((args) => args[0] === "preview-url").map((args) => args.at(-1)),
|
||||
["3986"],
|
||||
);
|
||||
assert.equal(fetched.length, 1);
|
||||
assert.equal(fetched[0]?.url, "https://admin-preview.example.test/health");
|
||||
assert.match(fetched[0]?.authorization ?? "", /^Bearer [a-f0-9]{64}$/);
|
||||
const healthCommand = calls.find((args) => args[0] === "exec" && args.at(-1)?.includes("curl -sf"))?.at(-1);
|
||||
assert.match(healthCommand ?? "", /curl -sf -H "Authorization: Bearer [a-f0-9]{64}" http:\/\/127\.0\.0\.1:3986\/health/);
|
||||
}
|
||||
|
||||
calls.length = 0;
|
||||
fetched.length = 0;
|
||||
await using publicLink = await denLink(
|
||||
{ apiUrl: "https://den.example.test/api/den", webUrl: "https://den.example.test" },
|
||||
{ sandboxId: "desktop-sandbox", daytonaExec: exec },
|
||||
);
|
||||
assert.deepEqual(publicLink.ref, {
|
||||
apiUrl: "https://data-preview.example.test/api/den",
|
||||
webUrl: "https://data-preview.example.test",
|
||||
});
|
||||
assert.deepEqual(
|
||||
calls.filter((args) => args[0] === "preview-url").map((args) => args.at(-1)),
|
||||
["3985", "3986"],
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("denLink admin rejects missing and wrong bearer tokens without mutating state", async () => {
|
||||
const upstream = createServer((_request, response) => response.end("ok"));
|
||||
const port = await listen(upstream);
|
||||
const originalFetch = globalThis.fetch;
|
||||
let adminHealthUrl = "";
|
||||
let authorization = "";
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = String(input);
|
||||
const header = new Headers(init?.headers).get("authorization");
|
||||
if (url.endsWith("/health") && header !== null) {
|
||||
adminHealthUrl = url;
|
||||
authorization = header;
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
try {
|
||||
await using link = await denLink({
|
||||
apiUrl: `http://127.0.0.1:${port}/api/den`,
|
||||
webUrl: `http://127.0.0.1:${port}`,
|
||||
});
|
||||
await link.admin.phase("protected");
|
||||
assert.match(authorization, /^Bearer [a-f0-9]{64}$/);
|
||||
const phaseUrl = adminHealthUrl.replace(/\/health$/, "/phase");
|
||||
for (const headers of [
|
||||
{ "content-type": "application/json" },
|
||||
{ authorization: "Bearer " + "b".repeat(64), "content-type": "application/json" },
|
||||
]) {
|
||||
const denied = await originalFetch(phaseUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ name: "attacker" }),
|
||||
});
|
||||
assert.equal(denied.status, 401);
|
||||
assert.deepEqual(await denied.json(), { error: "Unauthorized" });
|
||||
}
|
||||
assert.equal((await link.admin.health()).phase, "protected");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
await close(upstream);
|
||||
}
|
||||
});
|
||||
|
||||
test("denLink shapes a local Den connection and records phases", { timeout: 25_000 }, async () => {
|
||||
const upstream = createServer((request, response) => {
|
||||
if (request.url === "/large") {
|
||||
response.writeHead(200, { "content-length": LARGE_BODY.length, "content-type": "application/octet-stream" });
|
||||
response.end(LARGE_BODY);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/stream") {
|
||||
response.writeHead(200, { "content-type": "application/octet-stream" });
|
||||
response.write(Buffer.alloc(16 * 1024, 98));
|
||||
let writes = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (writes === 9) {
|
||||
clearInterval(timer);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
writes += 1;
|
||||
response.write(Buffer.alloc(16 * 1024, 98));
|
||||
}, 200);
|
||||
response.once("close", () => clearInterval(timer));
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, { "content-type": "application/json", "x-upstream": "yes" });
|
||||
response.end(JSON.stringify({ ok: true, path: request.url }));
|
||||
});
|
||||
const port = await listen(upstream);
|
||||
try {
|
||||
await using link = await denLink({
|
||||
apiUrl: `http://127.0.0.1:${port}/api/den`,
|
||||
webUrl: `http://127.0.0.1:${port}`,
|
||||
});
|
||||
|
||||
const passed = await timedFetch(`${link.ref.webUrl}/passthrough`);
|
||||
assert.equal(passed.status, 200);
|
||||
assert.equal(passed.headers.get("x-upstream"), "yes");
|
||||
assert.deepEqual(await passed.json(), { ok: true, path: "/passthrough" });
|
||||
let log = await link.admin.requests();
|
||||
assert.deepEqual(
|
||||
{ faulted: log.requests[0]?.faulted, phase: log.requests[0]?.phase },
|
||||
{ faulted: false, phase: "default" },
|
||||
);
|
||||
|
||||
await link.admin.rules([{ kind: "status", pathPrefix: "/status", statusCode: 503, times: 2, body: { error: "unavailable" } }]);
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/status`)).status, 503);
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/status`)).status, 503);
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/status`)).status, 200);
|
||||
log = await link.admin.requests();
|
||||
assert.deepEqual(log.requests.slice(-3).map(({ faulted, fault }) => ({ faulted, fault })), [
|
||||
{ faulted: true, fault: "status" },
|
||||
{ faulted: true, fault: "status" },
|
||||
{ faulted: false, fault: undefined },
|
||||
]);
|
||||
|
||||
await link.admin.rules([{ kind: "latency", pathPrefix: "/latency", delayMs: 300, jitterMs: 50 }]);
|
||||
const latencyStarted = Date.now();
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/latency`)).status, 200);
|
||||
const latencyElapsed = Date.now() - latencyStarted;
|
||||
assert(latencyElapsed >= 250, `Expected at least 250ms latency, got ${latencyElapsed}ms.`);
|
||||
assert(latencyElapsed <= 1_200, `Expected at most 1200ms latency, got ${latencyElapsed}ms.`);
|
||||
|
||||
await link.admin.rules([{ kind: "reset", pathPrefix: "/reset", everyNth: 2 }]);
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/reset`)).status, 200);
|
||||
await assert.rejects(timedFetch(`${link.ref.webUrl}/reset`));
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/reset`)).status, 200);
|
||||
await assert.rejects(timedFetch(`${link.ref.webUrl}/reset`));
|
||||
|
||||
await link.admin.phase("offline");
|
||||
await link.admin.offline(600);
|
||||
await assert.rejects(timedFetch(`${link.ref.webUrl}/offline`, 2_000));
|
||||
log = await link.admin.requests();
|
||||
assert((log.refusedConnections.offline ?? 0) >= 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/restored`)).status, 200);
|
||||
assert.equal((await link.admin.health()).offline, false);
|
||||
|
||||
await link.admin.clear();
|
||||
const streaming = await timedFetch(`${link.ref.webUrl}/stream`);
|
||||
assert(streaming.body);
|
||||
const reader = streaming.body.getReader();
|
||||
const firstChunk = await reader.read();
|
||||
assert.equal(firstChunk.done, false);
|
||||
await link.admin.offline(600);
|
||||
await assert.rejects(reader.read());
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
|
||||
await link.admin.bandwidth(64 * 1024);
|
||||
const cappedStarted = Date.now();
|
||||
const capped = await timedFetch(`${link.ref.webUrl}/large`, 8_000);
|
||||
const cappedBody = Buffer.from(await capped.arrayBuffer());
|
||||
const cappedElapsed = Date.now() - cappedStarted;
|
||||
assert(cappedElapsed >= 2_500, `Expected capped transfer to take at least 2500ms, got ${cappedElapsed}ms.`);
|
||||
assert.deepEqual(cappedBody, LARGE_BODY);
|
||||
assert.equal((await link.admin.requests()).requests.at(-1)?.fault, "bandwidth");
|
||||
|
||||
await link.admin.bandwidth(null);
|
||||
const fastStarted = Date.now();
|
||||
const fast = await timedFetch(`${link.ref.webUrl}/large`);
|
||||
assert.deepEqual(Buffer.from(await fast.arrayBuffer()), LARGE_BODY);
|
||||
assert(Date.now() - fastStarted < 1_000);
|
||||
|
||||
await link.admin.phase("b");
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/phase-b`)).status, 200);
|
||||
log = await link.admin.requests();
|
||||
assert.equal(log.requests.at(-1)?.phase, "b");
|
||||
|
||||
await link.admin.phase("vpn", "vpn-flaky-emulated");
|
||||
const vpnStarted = Date.now();
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/vpn-success-1`)).status, 200);
|
||||
const vpnDelay = Date.now() - vpnStarted;
|
||||
assert(vpnDelay >= 300, `Expected VPN emulation latency of at least 300ms, got ${vpnDelay}ms.`);
|
||||
assert(vpnDelay < 1_500, `Expected bounded VPN emulation latency, got ${vpnDelay}ms.`);
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/vpn-success-2`)).status, 200);
|
||||
await assert.rejects(timedFetch(`${link.ref.webUrl}/vpn-reset`));
|
||||
const stats = await link.admin.stats();
|
||||
assert.equal(stats.phase, "vpn");
|
||||
assert.equal(stats.profile, "vpn-flaky-emulated");
|
||||
assert(stats.faults >= 1);
|
||||
log = await link.admin.requests();
|
||||
assert.deepEqual(log.requests.slice(-3).map(({ fault, profile }) => ({ fault, profile })), [
|
||||
{ fault: "latency", profile: "vpn-flaky-emulated" },
|
||||
{ fault: "latency", profile: "vpn-flaky-emulated" },
|
||||
{ fault: "reset", profile: "vpn-flaky-emulated" },
|
||||
]);
|
||||
await link.admin.phase("baseline", "baseline");
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/baseline-restored`)).status, 200);
|
||||
const baselineEntry = (await link.admin.requests()).requests.at(-1);
|
||||
assert.deepEqual(
|
||||
baselineEntry && { path: baselineEntry.path, faulted: baselineEntry.faulted, phase: baselineEntry.phase, profile: baselineEntry.profile },
|
||||
{ path: "/baseline-restored", faulted: false, phase: "baseline", profile: "baseline" },
|
||||
);
|
||||
|
||||
await link.admin.rules([{ kind: "status", statusCode: 500, times: 10 }]);
|
||||
await link.admin.clear();
|
||||
assert.equal((await timedFetch(`${link.ref.webUrl}/cleared`)).status, 200);
|
||||
assert.equal((await link.admin.health()).offline, false);
|
||||
} finally {
|
||||
await close(upstream);
|
||||
}
|
||||
});
|
||||
|
||||
test("denLink pins absolute-form request targets to its upstream", async () => {
|
||||
let attackerRequests = 0;
|
||||
let upstreamRequestUrl: string | undefined;
|
||||
const upstream = createServer((request, response) => {
|
||||
upstreamRequestUrl = request.url;
|
||||
response.end("upstream");
|
||||
});
|
||||
const attacker = createServer((_request, response) => {
|
||||
attackerRequests += 1;
|
||||
response.end("attacker");
|
||||
});
|
||||
const [upstreamPort, attackerPort] = await Promise.all([listen(upstream), listen(attacker)]);
|
||||
try {
|
||||
await using link = await denLink({
|
||||
apiUrl: `http://127.0.0.1:${upstreamPort}/api/den`,
|
||||
webUrl: `http://127.0.0.1:${upstreamPort}`,
|
||||
});
|
||||
assert.equal(
|
||||
await absoluteGet(link.ref.webUrl, `http://127.0.0.1:${attackerPort}/steered?x=1`),
|
||||
"upstream",
|
||||
);
|
||||
assert.equal(upstreamRequestUrl, "/steered?x=1");
|
||||
assert.equal(attackerRequests, 0);
|
||||
} finally {
|
||||
await Promise.all([close(upstream), close(attacker)]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import type { Place } from "@openwork/env";
|
||||
import { selfHostServer } from "../src/self-host.ts";
|
||||
|
||||
test("selfHostServer skips Daytona placement", async () => {
|
||||
const place: Place = {
|
||||
kind: "daytona",
|
||||
host() {
|
||||
throw new Error("host must not be called");
|
||||
},
|
||||
async db() {
|
||||
throw new Error("db must not be called");
|
||||
},
|
||||
async exposeMock() {
|
||||
throw new Error("exposeMock must not be called");
|
||||
},
|
||||
denBase() {
|
||||
throw new Error("denBase must not be called");
|
||||
},
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
selfHostServer({ place, name: "Test", slug: "test", ownerEmails: ["owner@example.test"] }),
|
||||
(error: unknown) => error instanceof Error && error.name === "SkipError" && error.message.includes("OPENWORK_EVAL_DAYTONA"),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { electronProfilePaths } from "@openwork/hosts";
|
||||
import { readConnectStateFile } from "../src/state.ts";
|
||||
import type { CdpClient, Surface } from "@openwork/cdp";
|
||||
|
||||
const MISSING = "__OPENWORK_TESTKIT_MISSING__";
|
||||
|
||||
function surface(hostKind: string, profileDir?: string, sandboxId?: string): Surface {
|
||||
const client: CdpClient = {
|
||||
async send() {
|
||||
throw new Error("CDP must not be called.");
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
return {
|
||||
handle: { name: "desktop", kind: "electron", hostKind, cdpUrl: "http://127.0.0.1:1", profileDir, sandboxId },
|
||||
client,
|
||||
};
|
||||
}
|
||||
|
||||
test("readConnectStateFile reads Daytona candidates in order", async () => {
|
||||
const calls: Array<{ sandbox: string; script: string }> = [];
|
||||
const app = surface("daytona", "/workspace/profiles/desktop", "sandbox-1");
|
||||
const result = await readConnectStateFile(app, {
|
||||
exec: async (sandbox, script) => {
|
||||
calls.push({ sandbox, script });
|
||||
return calls.length === 1 ? MISSING : '{"connectEnabled":true}';
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "available", connectEnabled: true });
|
||||
assert.deepEqual(calls.map(({ sandbox }) => sandbox), ["sandbox-1", "sandbox-1"]);
|
||||
assert(calls[0]?.script.includes("/workspace/profiles/desktop/electron-userdata/openwork-dev-data/xdg/config/openwork/connect-state.json"));
|
||||
assert(calls[1]?.script.includes("/workspace/profiles/desktop/xdg-config/openwork/connect-state.json"));
|
||||
assert(calls.every(({ script }) => !script.includes("'")));
|
||||
});
|
||||
|
||||
test("readConnectStateFile reports non-JSON Daytona content as invalid", async () => {
|
||||
const result = await readConnectStateFile(surface("daytona", "/workspace/profile", "sandbox"), { exec: async () => "not-json" });
|
||||
assert.deepEqual(result, { status: "invalid", connectEnabled: null });
|
||||
});
|
||||
|
||||
test("readConnectStateFile reports missing when every Daytona candidate is absent", async () => {
|
||||
let calls = 0;
|
||||
const result = await readConnectStateFile(surface("daytona", "/workspace/profile", "sandbox"), {
|
||||
exec: async () => {
|
||||
calls += 1;
|
||||
return MISSING;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, { status: "missing", connectEnabled: null });
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
|
||||
test("readConnectStateFile requires a boolean connectEnabled", async () => {
|
||||
const result = await readConnectStateFile(surface("daytona", "/workspace/profile", "sandbox"), { exec: async () => "{}" });
|
||||
assert.deepEqual(result, { status: "invalid", connectEnabled: null });
|
||||
});
|
||||
|
||||
test("readConnectStateFile rejects unsupported host kinds", async () => {
|
||||
await assert.rejects(readConnectStateFile(surface("attached", "/tmp/profile")), /supports local and daytona/);
|
||||
});
|
||||
|
||||
test("readConnectStateFile requires a Daytona sandbox ID", async () => {
|
||||
await assert.rejects(readConnectStateFile(surface("daytona", "/workspace/profile")), /daytona app did not expose its sandbox ID/);
|
||||
});
|
||||
|
||||
test("readConnectStateFile rejects unsafe Daytona profile paths without executing", async () => {
|
||||
let called = false;
|
||||
await assert.rejects(
|
||||
readConnectStateFile(surface("daytona", "/workspace/unsafe profile", "sandbox"), {
|
||||
exec: async () => {
|
||||
called = true;
|
||||
return MISSING;
|
||||
},
|
||||
}),
|
||||
/Unsafe connect-state path.*unsafe profile/,
|
||||
);
|
||||
assert.equal(called, false);
|
||||
});
|
||||
|
||||
test("readConnectStateFile preserves local profile reads", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "openwork-state-test-"));
|
||||
try {
|
||||
const paths = electronProfilePaths(root);
|
||||
const statePath = join(paths.configHome, "openwork", "connect-state.json");
|
||||
await mkdir(dirname(statePath), { recursive: true });
|
||||
await writeFile(statePath, '{"connectEnabled":false}', "utf8");
|
||||
assert.deepEqual(await readConnectStateFile(surface("local", root)), { status: "available", connectEnabled: false });
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
checkNeeds,
|
||||
deriveMockEnv,
|
||||
ephemeralDatabaseName,
|
||||
needs,
|
||||
resolvePlace,
|
||||
SkipError,
|
||||
trustedOrigins,
|
||||
} from "@openwork/env";
|
||||
|
||||
test("resolvePlace selects local unless OPENWORK_EVAL_DAYTONA is exactly 1", () => {
|
||||
const local = resolvePlace({});
|
||||
const falseyDaytona = resolvePlace({ OPENWORK_EVAL_DAYTONA: "0" });
|
||||
const daytona = resolvePlace({ OPENWORK_EVAL_DAYTONA: "1", OPENWORK_EVAL_REF: "feature-ref" });
|
||||
assert.equal(local.kind, "local");
|
||||
assert.equal(falseyDaytona.kind, "local");
|
||||
assert.equal(daytona.kind, "daytona");
|
||||
assert.deepEqual(daytona.denBase(), { kind: "daytona", ref: "feature-ref" });
|
||||
});
|
||||
|
||||
test("needs accepts a tool-capable model and provider key", () => {
|
||||
assert.doesNotThrow(() => checkNeeds(
|
||||
{ model: "tool-capable", env: ["EXTRA_REQUIRED"] },
|
||||
{
|
||||
OPENWORK_EVAL_MODEL: "openai/gpt-5",
|
||||
OPENAI_API_KEY: "test-key",
|
||||
EXTRA_REQUIRED: "1",
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
test("needs throws a named SkipError for every unsatisfied resource", () => {
|
||||
assert.throws(
|
||||
() => checkNeeds({ model: "tool-capable", env: ["EXTRA_REQUIRED"], optIn: ["EXACT_OPT_IN"], daytona: true }, {}),
|
||||
(error) => {
|
||||
assert(error instanceof SkipError);
|
||||
assert.match(error.message, /^needs: /);
|
||||
assert.match(error.message, /set EXTRA_REQUIRED/);
|
||||
assert.match(error.message, /set EXACT_OPT_IN=1/);
|
||||
assert.match(error.message, /set OPENWORK_EVAL_MODEL/);
|
||||
assert.match(error.message, /set OPENAI_API_KEY or ANTHROPIC_API_KEY/);
|
||||
assert.match(error.message, /set OPENWORK_EVAL_DAYTONA=1/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("needs only accepts opt-in gates set exactly to 1", () => {
|
||||
assert.throws(() => checkNeeds({ optIn: ["EXACT_OPT_IN"] }, { EXACT_OPT_IN: "true" }), SkipError);
|
||||
assert.doesNotThrow(() => checkNeeds({ optIn: ["EXACT_OPT_IN"] }, { EXACT_OPT_IN: "1" }));
|
||||
});
|
||||
|
||||
test("needs reports an unavailable command", () => {
|
||||
const command = "openwork-impossible-command-for-testkit-test";
|
||||
assert.throws(
|
||||
() => checkNeeds({ commands: [command] }, {}),
|
||||
(error) => error instanceof SkipError && error.message.includes(`install ${command}`),
|
||||
);
|
||||
});
|
||||
|
||||
test("needs rejects a local-only test on Daytona or an attached Den", () => {
|
||||
assert.doesNotThrow(() => checkNeeds({ placement: "local" }, {}));
|
||||
assert.throws(() => checkNeeds({ placement: "local" }, { OPENWORK_EVAL_DAYTONA: "1" }), SkipError);
|
||||
assert.throws(
|
||||
() => checkNeeds({ placement: "local" }, { OPENWORK_EVAL_DEN_API_URL: "https://den.example.test" }),
|
||||
SkipError,
|
||||
);
|
||||
});
|
||||
|
||||
test("needs reads process.env at the call site", () => {
|
||||
const name = "OPENWORK_TESTKIT_UNIT_RESOURCE";
|
||||
const previous = process.env[name];
|
||||
try {
|
||||
delete process.env[name];
|
||||
assert.throws(() => needs({ env: [name] }), SkipError);
|
||||
process.env[name] = "available";
|
||||
assert.deepEqual(needs({ env: [name] }), {});
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env[name];
|
||||
else process.env[name] = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("mcp mock environment is derived from the resource name and public URLs", () => {
|
||||
assert.deepEqual(
|
||||
deriveMockEnv("acme tickets", "https://mock.example.test", "https://mock.example.test/mcp"),
|
||||
{
|
||||
OPENWORK_EVAL_MOCK_ACME_TICKETS_URL: "https://mock.example.test",
|
||||
OPENWORK_EVAL_MOCK_ACME_TICKETS_MCP_URL: "https://mock.example.test/mcp",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("trusted origins contain both Den ports in localhost and loopback forms", () => {
|
||||
assert.deepEqual(trustedOrigins(8788, 3005), [
|
||||
"http://localhost:8788",
|
||||
"http://127.0.0.1:8788",
|
||||
"http://localhost:3005",
|
||||
"http://127.0.0.1:3005",
|
||||
]);
|
||||
});
|
||||
|
||||
test("ephemeral database names are valid and unique", () => {
|
||||
const names = new Set(Array.from({ length: 100 }, () => ephemeralDatabaseName()));
|
||||
assert.equal(names.size, 100);
|
||||
for (const name of names) assert.match(name, /^[a-z][a-z0-9_]{0,62}$/);
|
||||
});
|
||||
Reference in New Issue
Block a user