feat: add auth broker p0 skeleton
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
|
||||
type RunnerDisposition = "ready" | "infra-blocked" | "business-failed";
|
||||
|
||||
@@ -11,6 +12,9 @@ interface FailureContract {
|
||||
|
||||
const docPath = "docs/reference/auth-broker.md";
|
||||
const doc = readFileSync(docPath, "utf8");
|
||||
const rustMainPath = "src/components/microservices/auth-broker/src/main.rs";
|
||||
const rustCargoPath = "src/components/microservices/auth-broker/Cargo.toml";
|
||||
const cliAdapterPath = "scripts/src/auth-broker.ts";
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
@@ -20,6 +24,44 @@ function assertDocContains(text: string): void {
|
||||
assertCondition(doc.includes(text), `missing auth broker doc text: ${text}`);
|
||||
}
|
||||
|
||||
function runCli(args: string[], env: Record<string, string | undefined> = {}): Promise<{ status: number | null; stdout: string; stderr: string; json: Record<string, unknown> | null }> {
|
||||
const childEnv = { ...process.env, ...env };
|
||||
delete childEnv.GH_TOKEN;
|
||||
delete childEnv.GITHUB_TOKEN;
|
||||
delete childEnv.UNIDESK_AUTH_BROKER_URL;
|
||||
delete childEnv.AUTH_BROKER_URL;
|
||||
if (env.GH_TOKEN !== undefined) childEnv.GH_TOKEN = env.GH_TOKEN;
|
||||
if (env.GITHUB_TOKEN !== undefined) childEnv.GITHUB_TOKEN = env.GITHUB_TOKEN;
|
||||
if (env.UNIDESK_AUTH_BROKER_URL !== undefined) childEnv.UNIDESK_AUTH_BROKER_URL = env.UNIDESK_AUTH_BROKER_URL;
|
||||
if (env.AUTH_BROKER_URL !== undefined) childEnv.AUTH_BROKER_URL = env.AUTH_BROKER_URL;
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("bun", ["scripts/cli.ts", ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: childEnv,
|
||||
});
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
||||
child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
||||
child.on("error", reject);
|
||||
child.on("close", (status) => {
|
||||
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
||||
let json: Record<string, unknown> | null = null;
|
||||
try {
|
||||
json = JSON.parse(stdout) as Record<string, unknown>;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
resolve({ status, stdout, stderr: Buffer.concat(stderrChunks).toString("utf8"), json });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function dataOf(response: Record<string, unknown>): Record<string, unknown> {
|
||||
assertCondition(typeof response.data === "object" && response.data !== null && !Array.isArray(response.data), "CLI response data should be object", response);
|
||||
return response.data as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const requiredOperations = [
|
||||
"github.auth.status",
|
||||
"github.issue.list",
|
||||
@@ -109,12 +151,15 @@ function walk(value: unknown, path: string[] = []): void {
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
async function main(): Promise<void> {
|
||||
for (const heading of ["## Existing Paths", "## API", "## Permission Boundary", "## Audit Fields", "## Failure Semantics", "## D601 Dev Acceptance"]) {
|
||||
assertDocContains(heading);
|
||||
}
|
||||
for (const path of [
|
||||
"scripts/src/gh.ts",
|
||||
"scripts/src/auth-broker.ts",
|
||||
"src/components/microservices/auth-broker/Cargo.toml",
|
||||
"src/components/microservices/auth-broker/src/main.rs",
|
||||
"scripts/code-queue-pr-preflight-example.ts",
|
||||
"src/components/microservices/code-queue/src/runtime-preflight.ts",
|
||||
"scripts/src/code-queue.ts",
|
||||
@@ -138,9 +183,68 @@ function main(): void {
|
||||
assertCondition(samplePreflightResponse.prCapabilityContract.preflightMergesPr === false, "P0 preflight must not merge PRs", samplePreflightResponse.prCapabilityContract);
|
||||
walk(samplePreflightResponse);
|
||||
|
||||
for (const path of [rustCargoPath, rustMainPath, cliAdapterPath]) {
|
||||
assertCondition(existsSync(path), `required auth broker implementation file is missing: ${path}`);
|
||||
}
|
||||
const rustMain = readFileSync(rustMainPath, "utf8");
|
||||
const cliAdapter = readFileSync(cliAdapterPath, "utf8");
|
||||
assertCondition(rustMain.includes("GET") && rustMain.includes("/health"), "Rust skeleton should expose GET /health", rustMainPath);
|
||||
assertCondition(rustMain.includes("/v1/github/gh"), "Rust skeleton should expose credential-request endpoint", rustMainPath);
|
||||
assertCondition(rustMain.includes("/v1/github/pr-preflight"), "Rust skeleton should expose pr-preflight endpoint", rustMainPath);
|
||||
assertCondition(rustMain.includes("credential_value_printed: false"), "audit event must force credentialValuePrinted=false", rustMainPath);
|
||||
assertCondition(!rustMain.includes("GH_TOKEN") && !rustMain.includes("GITHUB_TOKEN"), "Rust skeleton must not read runner token env keys", rustMainPath);
|
||||
assertCondition(cliAdapter.includes("valuesRead: false") && cliAdapter.includes("valuesPrinted: false"), "CLI adapter must declare secret values unread/unprinted", cliAdapterPath);
|
||||
assertCondition(cliAdapter.includes("broker-needed") && cliAdapter.includes("auth-missing"), "CLI adapter must expose broker-needed/auth-missing shape", cliAdapterPath);
|
||||
|
||||
const noToken = await runCli(["auth-broker", "pr-preflight", "--repo", "pikasTech/unidesk", "--base", "master", "--head", "feature/auth-broker", "--issue", "59", "--dry-run"]);
|
||||
assertCondition(noToken.status === 1, "missing broker endpoint should exit 1", { status: noToken.status, stdout: noToken.stdout, stderr: noToken.stderr });
|
||||
assertCondition(noToken.json?.ok === false, "missing token response envelope should fail", noToken.json);
|
||||
const noTokenData = dataOf(noToken.json ?? {});
|
||||
assertCondition(noTokenData.failureKind === "auth-missing", "missing token should classify as auth-missing", noTokenData);
|
||||
assertCondition(noTokenData.degradedReason === "broker-needed", "missing token should classify as broker-needed", noTokenData);
|
||||
assertCondition(noTokenData.brokerNeeded === true, "missing token should set brokerNeeded", noTokenData);
|
||||
assertCondition(!noToken.stdout.includes("contract-secret-marker"), "missing-token response must not leak secret marker strings", noToken.stdout);
|
||||
|
||||
const brokerReady = await runCli([
|
||||
"auth-broker",
|
||||
"pr-preflight",
|
||||
"--repo",
|
||||
"pikasTech/unidesk",
|
||||
"--base",
|
||||
"master",
|
||||
"--head",
|
||||
"feature/auth-broker",
|
||||
"--issue",
|
||||
"59",
|
||||
"--dry-run",
|
||||
"--endpoint",
|
||||
"http://user:pass@127.0.0.1:4291?credential=abc",
|
||||
]);
|
||||
assertCondition(brokerReady.status === 0, "configured broker dry-run should exit 0", { status: brokerReady.status, stdout: brokerReady.stdout, stderr: brokerReady.stderr });
|
||||
assertCondition(brokerReady.json?.ok === true, "configured broker dry-run envelope should succeed", brokerReady.json);
|
||||
const readyData = dataOf(brokerReady.json ?? {});
|
||||
const tokenCoverage = readyData.tokenCoverage as Record<string, unknown>;
|
||||
const brokerCoverage = readyData.brokerCoverage as Record<string, unknown>;
|
||||
const prCapability = readyData.prCapabilityContract as Record<string, unknown>;
|
||||
const brokerProxy = prCapability.brokerProxy as Record<string, unknown>;
|
||||
assertCondition(tokenCoverage.source === "auth-broker", "ready token coverage should come from broker", tokenCoverage);
|
||||
assertCondition(tokenCoverage.runnerEnvTokenRequired === false, "ready token coverage should not require runner env token", tokenCoverage);
|
||||
assertCondition(tokenCoverage.valuesPrinted === false, "ready token coverage must not print values", tokenCoverage);
|
||||
assertCondition(String(brokerCoverage.endpoint).includes("http://***:***@127.0.0.1:4291/?..."), "endpoint should be sanitized", brokerCoverage);
|
||||
assertCondition(prCapability.targetBranch === "master", "P0 capability should preserve target branch", prCapability);
|
||||
assertCondition(prCapability.preflightCreatesPr === false && prCapability.preflightMergesPr === false, "P0 PR preflight must not write or merge", prCapability);
|
||||
assertCondition(brokerProxy.writesRemote === false, "P0 broker proxy should not write remote", brokerProxy);
|
||||
assertCondition(Array.isArray(brokerProxy.operations) && brokerProxy.operations.includes("github.pr.create"), "P0 broker proxy should include PR create dry-run operation", brokerProxy);
|
||||
walk(readyData);
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
docPath,
|
||||
implementation: {
|
||||
rustMainPath,
|
||||
rustCargoPath,
|
||||
cliAdapterPath,
|
||||
},
|
||||
operations: requiredOperations,
|
||||
failureKinds: failureContracts.map((item) => item.failureKind),
|
||||
p0Safety: {
|
||||
@@ -152,4 +256,7 @@ function main(): void {
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user