626 lines
23 KiB
JavaScript
626 lines
23 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { argon2 } from "node:crypto";
|
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
import { createServer as createHttpServer } from "node:http";
|
|
import { request as httpsRequest } from "node:https";
|
|
import { createServer } from "node:net";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { startFakeOidcProvider } from "../../../backend/test/fixtures/oidc-provider.mjs";
|
|
|
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
const repositoryRoot = resolve(__dir, "../../..");
|
|
const frontendRoot = join(repositoryRoot, "frontend");
|
|
const backendRoot = join(repositoryRoot, "backend");
|
|
const harnessRoot = join(repositoryRoot, "harness");
|
|
const thtRoot = join(repositoryRoot, "tools", "tht");
|
|
const fakePi = join(frontendRoot, "e2e", "fixtures", "fake-pi.mjs");
|
|
const fakeTht = join(frontendRoot, "e2e", "fixtures", "fake-tht.mjs");
|
|
const FIXTURE_CLIENT_ID = "thothii-e2e-client";
|
|
const DEFAULT_FIXTURE_CLIENT_SECRET = "e2e-client-secret-not-a-production-secret";
|
|
const DEFAULT_FIXTURE_API_TOKEN = "e2e-group-catalog-token-not-a-production-secret";
|
|
const WRONG_CLIENT_ID = "thothii-e2e-unregistered-client";
|
|
const WRONG_CLIENT_SECRET = "e2e-wrong-client-secret-not-production";
|
|
const WRONG_API_TOKEN = "e2e-wrong-api-token-not-production";
|
|
const OIDC_CREDENTIAL_VARIANTS = new Set([
|
|
"correct", "wrong-client-id", "wrong-client-secret", "wrong-api-token",
|
|
]);
|
|
|
|
function safeError(code) {
|
|
return new Error(code);
|
|
}
|
|
|
|
function resolveFixtureCredentials() {
|
|
const runtimeCredential = process.env.THT_TASK15_SENTINEL;
|
|
if (runtimeCredential === undefined) {
|
|
return Object.freeze({
|
|
clientSecret: DEFAULT_FIXTURE_CLIENT_SECRET,
|
|
apiToken: DEFAULT_FIXTURE_API_TOKEN,
|
|
runtime: false,
|
|
});
|
|
}
|
|
if (runtimeCredential.length < 24 || runtimeCredential.length > 512 || /[\r\n\0]/u.test(runtimeCredential)) {
|
|
throw safeError("e2e_runtime_fixture_credential_invalid");
|
|
}
|
|
return Object.freeze({ clientSecret: runtimeCredential, apiToken: runtimeCredential, runtime: true });
|
|
}
|
|
|
|
function buildAuthenticationStorageBridge(output) {
|
|
const result = spawn("go", ["build", "-o", output, "./cmd/tht"], {
|
|
cwd: thtRoot,
|
|
stdio: "ignore",
|
|
});
|
|
return new Promise((resolveBuild, rejectBuild) => {
|
|
result.once("error", () => rejectBuild(safeError("e2e_auth_storage_build_failed")));
|
|
result.once("exit", (code) => code === 0 ? resolveBuild() : rejectBuild(safeError("e2e_auth_storage_build_failed")));
|
|
});
|
|
}
|
|
|
|
function secureDirectory(path) {
|
|
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
chmodSync(path, 0o700);
|
|
}
|
|
|
|
function runFixtureCommand(command, args, cwd) {
|
|
const child = spawn(command, args, { cwd, stdio: "ignore" });
|
|
return new Promise((resolveCommand, rejectCommand) => {
|
|
child.once("error", () => rejectCommand(safeError("e2e_workspace_fixture_command_failed")));
|
|
child.once("exit", (code) => code === 0
|
|
? resolveCommand()
|
|
: rejectCommand(safeError("e2e_workspace_fixture_command_failed")));
|
|
});
|
|
}
|
|
|
|
const F1_WORKSPACE_ID = "fixture-workspace";
|
|
const F1_WORKSPACE_DESCRIPTOR = `workspace:
|
|
schema_version: 3
|
|
id: fixture-workspace
|
|
name: Fixture workspace
|
|
language: en
|
|
dwh:
|
|
engine: postgres
|
|
database: fixture
|
|
schema: fixture
|
|
supported_transports: [postgres_direct]
|
|
semantic_index:
|
|
vector_store:
|
|
engine: qdrant
|
|
collection: fixture-workspace
|
|
dimensions: 1024
|
|
distance: cosine
|
|
embedding:
|
|
provider: ollama_internal
|
|
model: qwen3-embedding:0.6b
|
|
dimensions: 1024
|
|
llm_policy:
|
|
allowed: [zai/glm-5.2]
|
|
default: zai/glm-5.2
|
|
`;
|
|
|
|
async function prepareF1Workspace(root) {
|
|
const source = join(root, "workspace-source");
|
|
const remote = join(root, "workspace-remote.git");
|
|
secureDirectory(source);
|
|
secureDirectory(join(source, F1_WORKSPACE_ID));
|
|
writeSecure(join(source, "thoth-workspaces.yaml"), [
|
|
"schema_version: 1",
|
|
"workspaces:",
|
|
` - id: ${F1_WORKSPACE_ID}`,
|
|
" name: Fixture workspace",
|
|
"",
|
|
].join("\n"));
|
|
writeSecure(join(source, F1_WORKSPACE_ID, "workspace.yaml"), F1_WORKSPACE_DESCRIPTOR);
|
|
|
|
await runFixtureCommand("git", ["init", "--bare", "--initial-branch=main", remote], root);
|
|
chmodSync(remote, 0o700);
|
|
await runFixtureCommand("git", ["init", "--initial-branch=main"], source);
|
|
await runFixtureCommand("git", ["config", "user.name", "ThothII E2E Fixture"], source);
|
|
await runFixtureCommand("git", ["config", "user.email", "thothii-e2e@example.invalid"], source);
|
|
await runFixtureCommand("git", ["add", "-A"], source);
|
|
await runFixtureCommand("git", ["commit", "-m", "Create deterministic fixture workspace"], source);
|
|
await runFixtureCommand("git", ["remote", "add", "origin", remote], source);
|
|
await runFixtureCommand("git", ["push", "origin", "main"], source);
|
|
return { id: F1_WORKSPACE_ID, remote };
|
|
}
|
|
|
|
function writeSecure(path, value) {
|
|
writeFileSync(path, value, { encoding: "utf8", mode: 0o600 });
|
|
chmodSync(path, 0o600);
|
|
}
|
|
|
|
async function testPasswordHash(password) {
|
|
const salt = Buffer.from("thothii-e2e-salt");
|
|
const message = Buffer.from(password, "utf8");
|
|
let digest;
|
|
try {
|
|
digest = await new Promise((resolveDigest, rejectDigest) => {
|
|
argon2("argon2id", {
|
|
message,
|
|
nonce: salt,
|
|
memory: 65_536,
|
|
passes: 3,
|
|
parallelism: 1,
|
|
tagLength: 32,
|
|
}, (error, derived) => error || !derived ? rejectDigest(error ?? safeError("e2e_password_hash_failed")) : resolveDigest(derived));
|
|
});
|
|
return `$argon2id$v=19$m=65536,t=3,p=1$${salt.toString("base64").replaceAll("=", "")}$${digest.toString("base64").replaceAll("=", "")}`;
|
|
} finally {
|
|
message.fill(0);
|
|
salt.fill(0);
|
|
digest?.fill(0);
|
|
}
|
|
}
|
|
|
|
function pause(milliseconds) {
|
|
return new Promise((resolvePause) => setTimeout(resolvePause, milliseconds));
|
|
}
|
|
|
|
function providerJson(url, caFile, options = {}) {
|
|
return new Promise((resolveResponse, rejectResponse) => {
|
|
const body = options.body ?? "";
|
|
const request = httpsRequest(url, {
|
|
method: options.method ?? "GET",
|
|
ca: readFileSync(caFile),
|
|
headers: {
|
|
accept: "application/json",
|
|
...(body.length === 0 ? {} : {
|
|
"content-length": String(Buffer.byteLength(body)),
|
|
"content-type": "application/x-www-form-urlencoded",
|
|
}),
|
|
...options.headers,
|
|
},
|
|
}, (response) => {
|
|
const chunks = [];
|
|
let size = 0;
|
|
response.on("data", (chunk) => {
|
|
size += chunk.length;
|
|
if (size > 64 * 1024) request.destroy(safeError("e2e_provider_response_too_large"));
|
|
else chunks.push(chunk);
|
|
});
|
|
response.once("error", () => rejectResponse(safeError("e2e_provider_response_failed")));
|
|
response.once("end", () => {
|
|
try {
|
|
resolveResponse({ status: response.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) });
|
|
} catch {
|
|
rejectResponse(safeError("e2e_provider_response_invalid"));
|
|
}
|
|
});
|
|
});
|
|
request.once("error", () => rejectResponse(safeError("e2e_provider_request_failed")));
|
|
request.end(body);
|
|
});
|
|
}
|
|
|
|
async function freeLoopbackPort() {
|
|
const server = createServer();
|
|
await new Promise((resolveListen, rejectListen) => {
|
|
server.once("error", rejectListen);
|
|
server.listen({ host: "127.0.0.1", port: 0 }, resolveListen);
|
|
});
|
|
const address = server.address();
|
|
await new Promise((resolveClose) => server.close(resolveClose));
|
|
if (!address || typeof address === "string") throw safeError("e2e_loopback_port_unavailable");
|
|
return address.port;
|
|
}
|
|
|
|
const F1_QDRANT_INDEXES = Object.freeze([
|
|
"content_hash", "document_id", "kind", "record_key",
|
|
"record_kind", "vector_generation", "workspace_id", "workspace_revision",
|
|
]);
|
|
|
|
async function startFakeQdrant() {
|
|
const payloadSchema = Object.fromEntries(F1_QDRANT_INDEXES.map((field) => [field, { data_type: "keyword" }]));
|
|
const server = createHttpServer((request, response) => {
|
|
const path = new URL(request.url ?? "/", "http://loopback.invalid").pathname;
|
|
if (request.method !== "GET" || path !== `/collections/${F1_WORKSPACE_ID}`) {
|
|
response.writeHead(404).end();
|
|
return;
|
|
}
|
|
response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
|
|
result: {
|
|
config: { params: { vectors: { size: 1024, distance: "Cosine" } } },
|
|
payload_schema: payloadSchema,
|
|
},
|
|
}));
|
|
});
|
|
await new Promise((resolveListen, rejectListen) => {
|
|
server.once("error", rejectListen);
|
|
server.listen({ host: "127.0.0.1", port: 0 }, resolveListen);
|
|
});
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
await new Promise((resolveClose) => server.close(resolveClose));
|
|
throw safeError("e2e_qdrant_loopback_port_unavailable");
|
|
}
|
|
return {
|
|
baseUrl: `http://127.0.0.1:${address.port}/`,
|
|
close: () => new Promise((resolveClose) => server.close(resolveClose)),
|
|
};
|
|
}
|
|
|
|
function managedProcess(command, args, options) {
|
|
const child = spawn(command, args, {
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let exited = false;
|
|
let diagnostic = "";
|
|
const captureDiagnostic = (chunk) => {
|
|
const sanitized = String(chunk)
|
|
.replace(/https?:\/\/[^\s)]+/g, "[url]")
|
|
.replace(/(?:THT_[A-Z_]+|PI_[A-Z_]+|AUTH_MODE)=\S+/g, "$1=[redacted]")
|
|
.replace(/[A-Za-z0-9_-]{43,}/g, "[redacted]");
|
|
diagnostic = `${diagnostic}${sanitized}`.slice(-800);
|
|
};
|
|
child.once("exit", () => { exited = true; });
|
|
child.stdout?.on("data", captureDiagnostic);
|
|
child.stderr?.on("data", captureDiagnostic);
|
|
return {
|
|
child,
|
|
exited: () => exited,
|
|
diagnostic: () => diagnostic.replace(/\s+/g, " ").trim(),
|
|
async close() {
|
|
if (exited) return;
|
|
child.kill("SIGTERM");
|
|
for (let attempt = 0; attempt < 50 && !exited; attempt += 1) await pause(20);
|
|
if (!exited) child.kill("SIGKILL");
|
|
for (let attempt = 0; attempt < 50 && !exited; attempt += 1) await pause(20);
|
|
},
|
|
};
|
|
}
|
|
|
|
function oneShotJson(command, args, options) {
|
|
return new Promise((resolveCommand) => {
|
|
const child = spawn(command, args, {
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
let output = "";
|
|
let outputValid = true;
|
|
let settled = false;
|
|
const finish = (status) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
let report;
|
|
try {
|
|
report = outputValid ? JSON.parse(output) : undefined;
|
|
} catch {
|
|
report = undefined;
|
|
}
|
|
resolveCommand({
|
|
status: Number.isInteger(status) ? status : 1,
|
|
report: report && typeof report === "object"
|
|
? report
|
|
: { ready: false, mode: "none", checks: [{ code: "fixture_command_invalid" }] },
|
|
});
|
|
};
|
|
child.stdout?.on("data", (chunk) => {
|
|
if (!outputValid) return;
|
|
output += String(chunk);
|
|
if (Buffer.byteLength(output) > 64 * 1024) {
|
|
output = "";
|
|
outputValid = false;
|
|
child.kill("SIGKILL");
|
|
}
|
|
});
|
|
child.once("error", () => finish(1));
|
|
child.once("exit", (status) => finish(status));
|
|
const timer = setTimeout(() => child.kill("SIGKILL"), 35_000);
|
|
});
|
|
}
|
|
|
|
async function waitForOk(url, processHandle) {
|
|
for (let attempt = 0; attempt < 300; attempt += 1) {
|
|
if (processHandle.exited()) {
|
|
const detail = processHandle.diagnostic();
|
|
throw safeError(detail ? `e2e_service_stopped_during_startup:${detail}` : "e2e_service_stopped_during_startup");
|
|
}
|
|
try {
|
|
const response = await fetch(url, { redirect: "error" });
|
|
if (response.ok) return;
|
|
} catch {
|
|
// The process is expected to race its listener setup.
|
|
}
|
|
await pause(100);
|
|
}
|
|
throw safeError("e2e_service_startup_timeout");
|
|
}
|
|
|
|
function cleanBackendEnvironment(overrides) {
|
|
const env = { ...process.env };
|
|
for (const name of [
|
|
"AUTH_MODE", "THT_AUTH_CONFIG_FILE", "THT_AUTH_STATE_ROOT", "THT_SECRETS_FILE", "NODE_EXTRA_CA_CERTS",
|
|
"SETTINGS_FILE", "THT_MAINTENANCE_FILE", "THT_WORKSPACE_REGISTRY_ROOT", "THT_WORKSPACE_SECRET_STORE_ROOT",
|
|
"THT_WORKSPACE_SECRET_RUNTIME_ROOT", "THT_WORKSPACE_GIT_REMOTE", "THT_WORKSPACE_GIT_BRANCH",
|
|
"THT_WORKSPACE_SECRET_ROOTS", "THT_WORKSPACE_INSTALLATION_ID", "THT_LEGACY_WORKSPACE_MODE",
|
|
"THT_DWH_PRECHECK", "THT_INTERNAL_QDRANT_URL", "THT_CONFIG", "THT_TASK15_SENTINEL",
|
|
]) delete env[name];
|
|
for (const name of Object.keys(env)) {
|
|
if (name.startsWith("THT_WS_")) delete env[name];
|
|
}
|
|
return { ...env, ...overrides };
|
|
}
|
|
|
|
export async function createAuthenticationStack({ withF1Workspace = false } = {}) {
|
|
const credentials = resolveFixtureCredentials();
|
|
const root = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-e2e-"));
|
|
secureDirectory(root);
|
|
const stateRoot = join(root, "auth-state");
|
|
const registryRoot = join(root, "workspace-registry");
|
|
const workspaceSecretRoot = join(root, "workspace-secrets");
|
|
const workspaceRuntimeRoot = join(root, "workspace-runtime");
|
|
const fixtureSecretRoot = join(root, "fixture-runtime-secrets");
|
|
const providerRoot = join(root, "provider");
|
|
const authConfigFile = join(root, "auth.yaml");
|
|
const usersFile = join(root, "users.yaml");
|
|
const secretsFile = join(root, "test.secrets");
|
|
const settingsFile = join(root, "settings.json");
|
|
const maintenanceFile = join(root, "maintenance.json");
|
|
const authStorageBinary = join(root, "tht-auth-storage");
|
|
const fixtureDwhPasswordFile = join(fixtureSecretRoot, "fixture-dwh-password");
|
|
const fixtureDwhCaFile = join(fixtureSecretRoot, "fixture-dwh-ca.pem");
|
|
for (const path of [stateRoot, registryRoot, workspaceSecretRoot, workspaceRuntimeRoot, fixtureSecretRoot, providerRoot]) secureDirectory(path);
|
|
for (const child of ["sessions", "oidc"]) secureDirectory(join(stateRoot, child));
|
|
|
|
const [frontendPort, backendPort] = await Promise.all([freeLoopbackPort(), freeLoopbackPort()]);
|
|
const publicUrl = `http://127.0.0.1:${frontendPort}`;
|
|
const backendUrl = `http://127.0.0.1:${backendPort}`;
|
|
const workspace = withF1Workspace ? await prepareF1Workspace(root) : undefined;
|
|
const provider = await startFakeOidcProvider({
|
|
directory: providerRoot,
|
|
registration: {
|
|
clientId: FIXTURE_CLIENT_ID,
|
|
clientSecret: credentials.clientSecret,
|
|
redirectUri: new URL("/api/auth/oidc/callback", publicUrl).href,
|
|
},
|
|
apiToken: credentials.apiToken,
|
|
});
|
|
await buildAuthenticationStorageBridge(authStorageBinary);
|
|
const localPassword = "e2e-local-password";
|
|
const passwordHash = await testPasswordHash(localPassword);
|
|
const accounts = Object.freeze({
|
|
ordinary: Object.freeze({ username: "ordinary", password: localPassword }),
|
|
admin: Object.freeze({ username: "administrator", password: localPassword }),
|
|
});
|
|
writeSecure(usersFile, JSON.stringify({
|
|
version: 1,
|
|
users: [
|
|
{
|
|
id: "11111111-1111-4111-8111-111111111111",
|
|
username: accounts.ordinary.username,
|
|
displayName: "Fixture ordinary",
|
|
passwordHash,
|
|
roles: ["user"],
|
|
enabled: true,
|
|
authRevision: 1,
|
|
},
|
|
{
|
|
id: "22222222-2222-4222-8222-222222222222",
|
|
username: accounts.admin.username,
|
|
displayName: "Fixture administrator",
|
|
passwordHash,
|
|
roles: ["admin"],
|
|
enabled: true,
|
|
authRevision: 1,
|
|
},
|
|
],
|
|
}));
|
|
function writeOidcSecrets(variant) {
|
|
if (!OIDC_CREDENTIAL_VARIANTS.has(variant)) throw safeError("e2e_oidc_credential_variant_invalid");
|
|
const selectedClientSecret = variant === "wrong-client-secret" ? WRONG_CLIENT_SECRET : credentials.clientSecret;
|
|
const selectedApiToken = variant === "wrong-api-token" ? WRONG_API_TOKEN : credentials.apiToken;
|
|
writeSecure(secretsFile, [
|
|
`THT_OIDC_CLIENT_SECRET=${selectedClientSecret}`,
|
|
`THT_AUTHENTIK_API_TOKEN=${selectedApiToken}`,
|
|
"",
|
|
].join("\n"));
|
|
}
|
|
writeOidcSecrets("correct");
|
|
if (workspace) {
|
|
writeSecure(fixtureDwhPasswordFile, "fixture-password-not-a-secret\n");
|
|
writeSecure(fixtureDwhCaFile, "fixture-ca-not-a-production-certificate\n");
|
|
writeSecure(settingsFile, JSON.stringify({
|
|
workspace: workspace.id,
|
|
provider: "zai",
|
|
model: "glm-5.2",
|
|
thinking: "medium",
|
|
}));
|
|
}
|
|
|
|
let mode = undefined;
|
|
let backend = undefined;
|
|
let qdrant = undefined;
|
|
const frontend = managedProcess(join(frontendRoot, "node_modules", ".bin", "vite"), [
|
|
"--host", "127.0.0.1", "--port", String(frontendPort), "--strictPort",
|
|
], {
|
|
cwd: frontendRoot,
|
|
env: {
|
|
...process.env,
|
|
THT_FRONTEND_API_UPSTREAM: backendUrl,
|
|
},
|
|
});
|
|
try {
|
|
await waitForOk(publicUrl, frontend);
|
|
qdrant = workspace ? await startFakeQdrant() : undefined;
|
|
|
|
const localConfig = () => ({
|
|
version: 1,
|
|
mode: "local",
|
|
publicUrl,
|
|
session: {
|
|
regularTtlSeconds: 600,
|
|
regularIdleSeconds: 600,
|
|
rememberTtlSeconds: 2_592_000,
|
|
rememberIdleSeconds: 604_800,
|
|
},
|
|
local: { usersFile: "users.yaml" },
|
|
});
|
|
const oidcConfig = (variant = "correct") => ({
|
|
version: 1,
|
|
mode: "oidc",
|
|
publicUrl,
|
|
session: {
|
|
regularTtlSeconds: 600,
|
|
regularIdleSeconds: 600,
|
|
oidcTtlSeconds: 60,
|
|
},
|
|
oidc: {
|
|
issuer: provider.issuer,
|
|
clientId: variant === "wrong-client-id" ? WRONG_CLIENT_ID : FIXTURE_CLIENT_ID,
|
|
clientSecretRef: "THT_OIDC_CLIENT_SECRET",
|
|
scopes: ["openid", "profile", "groups"],
|
|
groupsClaim: "groups",
|
|
},
|
|
groupCatalog: {
|
|
driver: "authentik",
|
|
baseUrl: provider.baseUrl,
|
|
apiTokenRef: "THT_AUTHENTIK_API_TOKEN",
|
|
},
|
|
authorization: {
|
|
groupRoles: {
|
|
"fixture-users": ["user"],
|
|
"fixture-admin": ["admin"],
|
|
},
|
|
},
|
|
});
|
|
|
|
function backendEnvironment() {
|
|
return cleanBackendEnvironment({
|
|
NODE_ENV: "test",
|
|
HOST: "127.0.0.1",
|
|
PORT: String(backendPort),
|
|
PI_BIN: fakePi,
|
|
THT_BIN: fakeTht,
|
|
THT_AUTH_STORAGE_BIN: authStorageBinary,
|
|
THT_HARNESS_DIR: harnessRoot,
|
|
THT_AUTH_CONFIG_FILE: authConfigFile,
|
|
THT_AUTH_STATE_ROOT: stateRoot,
|
|
THT_SECRETS_FILE: secretsFile,
|
|
NODE_EXTRA_CA_CERTS: provider.caFile,
|
|
SETTINGS_FILE: settingsFile,
|
|
THT_MAINTENANCE_FILE: maintenanceFile,
|
|
THT_WORKSPACE_REGISTRY_ROOT: registryRoot,
|
|
THT_WORKSPACE_SECRET_STORE_ROOT: workspaceSecretRoot,
|
|
THT_WORKSPACE_SECRET_RUNTIME_ROOT: workspaceRuntimeRoot,
|
|
THT_WORKSPACE_INSTALLATION_ID: "e2e",
|
|
THT_DATA_ROOT: join(root, "data"),
|
|
...(qdrant ? { THT_INTERNAL_QDRANT_URL: qdrant.baseUrl } : {}),
|
|
...(workspace ? {
|
|
THT_WORKSPACE_GIT_REMOTE: workspace.remote,
|
|
THT_WORKSPACE_GIT_BRANCH: "main",
|
|
THT_WORKSPACE_SECRET_ROOTS: fixtureSecretRoot,
|
|
THT_WS_FIXTURE_WORKSPACE_DWH_TRANSPORT: "postgres_direct",
|
|
THT_WS_FIXTURE_WORKSPACE_DWH_HOST: "127.0.0.1",
|
|
THT_WS_FIXTURE_WORKSPACE_DWH_PORT: "5432",
|
|
THT_WS_FIXTURE_WORKSPACE_DWH_USER: "fixture",
|
|
THT_WS_FIXTURE_WORKSPACE_DWH_PASSWORD_FILE: fixtureDwhPasswordFile,
|
|
THT_WS_FIXTURE_WORKSPACE_DWH_TLS_CA_FILE: fixtureDwhCaFile,
|
|
} : {}),
|
|
});
|
|
}
|
|
|
|
async function startBackend() {
|
|
if (mode === undefined) throw safeError("e2e_auth_mode_not_configured");
|
|
backend = managedProcess(join(backendRoot, "node_modules", ".bin", "tsx"), ["src/server.ts"], {
|
|
cwd: backendRoot,
|
|
env: backendEnvironment(),
|
|
});
|
|
await waitForOk(`${backendUrl}/health`, backend);
|
|
}
|
|
|
|
async function restartBackend() {
|
|
await backend?.close();
|
|
backend = undefined;
|
|
await startBackend();
|
|
}
|
|
|
|
return {
|
|
publicUrl,
|
|
localAccount(account) {
|
|
const found = accounts[account];
|
|
if (!found) throw safeError("e2e_local_account_unknown");
|
|
return found;
|
|
},
|
|
lastAuthorization() {
|
|
return provider.lastAuthorization();
|
|
},
|
|
async providerSurface() {
|
|
const discovery = await providerJson(`${provider.issuer}.well-known/openid-configuration`, provider.caFile);
|
|
const jwks = await providerJson(`${provider.issuer}jwks`, provider.caFile);
|
|
const device = await providerJson(`${provider.issuer}device_authorization`, provider.caFile, {
|
|
method: "POST",
|
|
body: new URLSearchParams({
|
|
client_id: FIXTURE_CLIENT_ID,
|
|
client_secret: credentials.clientSecret,
|
|
}).toString(),
|
|
});
|
|
const deviceCode = typeof device.body?.device_code === "string" ? device.body.device_code : "";
|
|
const deviceToken = deviceCode.length === 0 ? { status: 0, body: {} } : await providerJson(`${provider.issuer}token`, provider.caFile, {
|
|
method: "POST",
|
|
body: new URLSearchParams({
|
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
client_id: FIXTURE_CLIENT_ID,
|
|
client_secret: credentials.clientSecret,
|
|
device_code: deviceCode,
|
|
}).toString(),
|
|
});
|
|
const groups = await providerJson(`${provider.baseUrl}/api/v3/core/groups/?name=fixture-users`, provider.caFile, {
|
|
headers: { authorization: `Bearer ${credentials.apiToken}` },
|
|
});
|
|
return {
|
|
discovery: discovery.status === 200 && discovery.body?.issuer === provider.issuer,
|
|
jwks: jwks.status === 200 && jwks.body?.keys?.[0]?.alg === "RS256" && jwks.body?.keys?.[0]?.use === "sig",
|
|
deviceAuthorization: device.status === 200 && typeof device.body?.device_code === "string"
|
|
&& typeof device.body?.verification_uri === "string",
|
|
deviceToken: deviceToken.status === 200 && typeof deviceToken.body?.id_token === "string",
|
|
groupList: groups.status === 200 && groups.body?.results?.[0]?.name === "fixture-users",
|
|
runtimeCredential: credentials.runtime,
|
|
};
|
|
},
|
|
setOidcIdentity(identity) {
|
|
provider.setIdentity(identity);
|
|
},
|
|
async useLocalMode() {
|
|
writeSecure(authConfigFile, JSON.stringify(localConfig()));
|
|
mode = "local";
|
|
await restartBackend();
|
|
},
|
|
async useOidcMode(identity, variant = "correct") {
|
|
if (!OIDC_CREDENTIAL_VARIANTS.has(variant)) throw safeError("e2e_oidc_credential_variant_invalid");
|
|
provider.setIdentity(identity);
|
|
writeOidcSecrets(variant);
|
|
writeSecure(authConfigFile, JSON.stringify(oidcConfig(variant)));
|
|
mode = "oidc";
|
|
await restartBackend();
|
|
},
|
|
async authDiagnostics() {
|
|
if (mode !== "oidc") throw safeError("e2e_oidc_mode_required");
|
|
return oneShotJson(
|
|
join(backendRoot, "node_modules", ".bin", "tsx"),
|
|
["src/auth/diagnostic-command.ts", "--json"],
|
|
{ cwd: backendRoot, env: backendEnvironment() },
|
|
);
|
|
},
|
|
restartBackend,
|
|
async close() {
|
|
await backend?.close();
|
|
await frontend.close();
|
|
await provider.close();
|
|
await qdrant?.close();
|
|
rmSync(root, { recursive: true, force: true });
|
|
},
|
|
};
|
|
} catch (error) {
|
|
await backend?.close();
|
|
await frontend.close();
|
|
await provider.close();
|
|
await qdrant?.close();
|
|
rmSync(root, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
}
|