import { readFileSync } from "node:fs"; import { type UniDeskConfig, readConfig, rootPath } from "./config"; const DEFAULT_REPO = "pikasTech/unidesk"; const DEFAULT_BASE = "master"; const SECRET_ENV_KEYS = ["GH_TOKEN", "GITHUB_TOKEN"] as const; const BROKER_URL_ENV_KEYS = ["UNIDESK_AUTH_BROKER_URL", "AUTH_BROKER_URL"] as const; const BROKER_CREDENTIAL_REF_ENV_KEYS = ["UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF", "AUTH_BROKER_GITHUB_CREDENTIAL_REF"] as const; const BROKER_CONFIGURED_ENV_KEYS = ["UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED", "AUTH_BROKER_GITHUB_CONFIGURED"] as const; const AUTH_BROKER_SERVICE_ID = "auth-broker"; const DEFAULT_CAPABILITIES = [ "github.auth.status", "github.issue.list", "github.issue.read", "github.pr.list", "github.pr.read", "github.pr.create", "github.pr.comment.create", "github.pr.preflight.dry-run", ] as const; type BrokerCommand = "contract" | "credential-request" | "pr-preflight" | "health"; type RunnerDisposition = "ready" | "infra-blocked" | "business-failed"; type ConfigSource = "config.json" | "unavailable"; interface BrokerAdapterOptions { command: BrokerCommand; repo: string; operation: string; dryRun: boolean; endpoint: string | null; base: string; head: string; issueNumber: number | null; } interface AuthBrokerServiceRegistration { ok: boolean; serviceId: "auth-broker"; source: ConfigSource; configured: boolean; providerId: string | null; deploymentMode: string | null; proxyMode: string | null; backendBaseUrl: string | null; healthPath: string | null; allowedPathPrefixes: string[]; composeService: string | null; containerName: string | null; dockerfile: string | null; public: boolean | null; error: string | null; } interface ComposeProfileRegistration { ok: boolean; source: "docker-compose.yml"; serviceId: "auth-broker"; servicePresent: boolean; profileGated: boolean; profiles: string[]; restart: string | null; publicPortPublished: boolean; exposesPort: boolean; mutatesDefaultRuntime: false; } interface DeployManifestRegistration { ok: boolean; source: "deploy.json"; serviceId: "auth-broker"; prod: { present: boolean; commitId: string | null }; dev: { present: boolean; commitId: string | null }; dryRunOnly: true; } interface RuntimeCredentialRefPresence { ok: boolean; source: string | null; configuredFlag: { present: boolean; key: string | null; truthy: boolean; }; credentialRef: { present: boolean; key: string | null; valuePrinted: false; valuePreview: string | null; }; presenceOnly: true; valuesRead: false; valuesPrinted: false; } function hasEnvKey(name: string): boolean { return Object.prototype.hasOwnProperty.call(process.env, name); } function firstPresentEnvKey(keys: readonly string[]): string | null { for (const key of keys) { if (hasEnvKey(key)) return key; } return null; } function stringOption(args: string[], name: string): string | undefined { const index = args.indexOf(name); if (index === -1) return undefined; const value = args[index + 1]; if (value === undefined || value.length === 0) throw new Error(`${name} requires a non-empty value`); return value; } function sanitizeEndpoint(value: string): string { try { const parsed = new URL(value); if (parsed.username.length > 0) parsed.username = "***"; if (parsed.password.length > 0) parsed.password = "***"; if (parsed.search.length > 0) parsed.search = "?..."; parsed.hash = ""; return parsed.toString(); } catch { return value.includes("?") ? `${value.split("?")[0]}?...` : value; } } function sanitizeCredentialRef(value: string): string { const trimmed = value.trim(); if (trimmed.length === 0) return ""; const separator = trimmed.indexOf(":"); if (separator <= 0) return ""; return `${trimmed.slice(0, separator)}:`; } function numberOption(args: string[], name: string): number | null { const raw = stringOption(args, name); if (raw === undefined) return null; const value = Number(raw); if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`); return value; } function firstConfiguredBrokerUrl(): string | null { for (const key of BROKER_URL_ENV_KEYS) { if (hasEnvKey(key)) return `<${key}>`; } return null; } function envTruthy(key: string | null): boolean { if (key === null) return false; const value = process.env[key]?.trim().toLowerCase(); return value === "1" || value === "true" || value === "yes" || value === "on"; } function runtimeCredentialRefPresence(): RuntimeCredentialRefPresence { const configuredKey = firstPresentEnvKey(BROKER_CONFIGURED_ENV_KEYS); const refKey = firstPresentEnvKey(BROKER_CREDENTIAL_REF_ENV_KEYS); return { ok: configuredKey !== null && envTruthy(configuredKey) && refKey !== null, source: refKey === null ? null : "broker-held-github-credential-ref", configuredFlag: { present: configuredKey !== null, key: configuredKey, truthy: envTruthy(configuredKey), }, credentialRef: { present: refKey !== null, key: refKey, valuePrinted: false, valuePreview: refKey === null ? null : sanitizeCredentialRef(process.env[refKey] ?? ""), }, presenceOnly: true, valuesRead: false, valuesPrinted: false, }; } function readConfigRegistration(): AuthBrokerServiceRegistration { let config: UniDeskConfig; try { config = readConfig(); } catch (error) { return { ok: false, serviceId: AUTH_BROKER_SERVICE_ID, source: "unavailable", configured: false, providerId: null, deploymentMode: null, proxyMode: null, backendBaseUrl: null, healthPath: null, allowedPathPrefixes: [], composeService: null, containerName: null, dockerfile: null, public: null, error: error instanceof Error ? error.message : String(error), }; } const service = config.microservices.find((item) => item.id === AUTH_BROKER_SERVICE_ID); return { ok: service !== undefined, serviceId: AUTH_BROKER_SERVICE_ID, source: "config.json", configured: service !== undefined, providerId: service?.providerId ?? null, deploymentMode: service?.deployment.mode ?? null, proxyMode: service?.backend.proxyMode ?? null, backendBaseUrl: service?.backend.nodeBaseUrl ?? null, healthPath: service?.backend.healthPath ?? null, allowedPathPrefixes: service?.backend.allowedPathPrefixes ?? [], composeService: service?.repository.composeService ?? null, containerName: service?.repository.containerName ?? null, dockerfile: service?.repository.dockerfile ?? null, public: service?.backend.public ?? null, error: service === undefined ? "auth-broker is not registered in config.json microservices" : null, }; } function extractComposeServiceBlock(composeText: string, serviceName: string): string { const lines = composeText.split("\n"); const startLine = lines.findIndex((line) => line === ` ${serviceName}:`); if (startLine < 0) return ""; let endLine = lines.length; for (let index = startLine + 1; index < lines.length; index += 1) { if (/^ [A-Za-z0-9][A-Za-z0-9_-]*:$/u.test(lines[index] ?? "")) { endLine = index; break; } } return lines.slice(startLine, endLine).join("\n"); } function composeProfileRegistration(): ComposeProfileRegistration { const composeText = readFileSync(rootPath("docker-compose.yml"), "utf8"); const block = extractComposeServiceBlock(composeText, AUTH_BROKER_SERVICE_ID); const blockLines = block.split("\n"); const profiles: string[] = []; const profileStart = blockLines.findIndex((line) => /^\s{4}profiles:\s*$/u.test(line)); if (profileStart >= 0) { for (let index = profileStart + 1; index < blockLines.length; index += 1) { const line = blockLines[index] ?? ""; if (/^\s{4}[A-Za-z0-9_-]+:/u.test(line)) break; const profile = line.match(/^\s{6}-\s+"?([A-Za-z0-9_.-]+)"?\s*$/u)?.[1]; if (profile !== undefined) profiles.push(profile); } } const restart = block.match(/^\s{4}restart:\s+"?([^"\n]+)"?\s*$/mu)?.[1] ?? null; const portsSection = /^\s{4}ports:\s*$/mu.test(block); const portMapping = /^\s{6}-\s+["']?[^"'\n]*4291:/mu.test(block); return { ok: block.length > 0 && profiles.includes(AUTH_BROKER_SERVICE_ID) && !portsSection && !portMapping, source: "docker-compose.yml", serviceId: AUTH_BROKER_SERVICE_ID, servicePresent: block.length > 0, profileGated: profiles.includes(AUTH_BROKER_SERVICE_ID), profiles, restart, publicPortPublished: portsSection || portMapping, exposesPort: block.includes('"4291"'), mutatesDefaultRuntime: false, }; } function serviceCommitFromDeployJson(environment: "prod" | "dev"): string | null { const parsed = JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as { environments?: Record }>; }; const service = parsed.environments?.[environment]?.services?.find((item) => item.id === AUTH_BROKER_SERVICE_ID); return typeof service?.commitId === "string" ? service.commitId : null; } function deployManifestRegistration(): DeployManifestRegistration { const prodCommit = serviceCommitFromDeployJson("prod"); const devCommit = serviceCommitFromDeployJson("dev"); return { ok: prodCommit !== null && devCommit !== null, source: "deploy.json", serviceId: AUTH_BROKER_SERVICE_ID, prod: { present: prodCommit !== null, commitId: prodCommit }, dev: { present: devCommit !== null, commitId: devCommit }, dryRunOnly: true, }; } function serviceRegistrationContract(): Record { return { config: readConfigRegistration(), compose: composeProfileRegistration(), deploy: deployManifestRegistration(), runtimeCredentialRef: runtimeCredentialRefPresence(), defaultRuntimeMutation: { ok: true, mutatesCurrentProd: false, composeProfileRequired: AUTH_BROKER_SERVICE_ID, publicPortPublished: false, liveDeployPerformed: false, }, }; } function parseOptions(args: string[]): BrokerAdapterOptions { const rawCommand = args[0] ?? "contract"; if (!["contract", "credential-request", "pr-preflight", "health"].includes(rawCommand)) { throw new Error(`unknown auth-broker command: ${rawCommand}`); } const command = rawCommand as BrokerCommand; const endpoint = stringOption(args, "--endpoint"); return { command, repo: stringOption(args, "--repo") ?? DEFAULT_REPO, operation: stringOption(args, "--operation") ?? (command === "pr-preflight" ? "github.pr.preflight.dry-run" : "github.auth.status"), dryRun: args.includes("--dry-run") || command === "contract" || command === "credential-request" || command === "pr-preflight" || command === "health", endpoint: endpoint === undefined ? firstConfiguredBrokerUrl() : sanitizeEndpoint(endpoint), base: stringOption(args, "--base") ?? DEFAULT_BASE, head: stringOption(args, "--head") ?? "", issueNumber: numberOption(args, "--issue"), }; } function runnerEnvTokenCoverage(): Record { const present = SECRET_ENV_KEYS.filter((key) => hasEnvKey(key)); return { ok: present.length > 0, source: present.length > 0 ? "runner-env" : null, checkedKeys: SECRET_ENV_KEYS, presentKeys: present, missingKeys: SECRET_ENV_KEYS.filter((key) => !present.includes(key)), presenceOnly: true, valuesRead: false, valuesPrinted: false, }; } function brokerCoverage(endpoint: string | null): Record { const credential = runtimeCredentialRefPresence(); return { ok: endpoint !== null, source: endpoint === null ? null : "auth-broker", endpoint: endpoint ?? null, credentialRef: endpoint === null ? null : credential.credentialRef.valuePreview ?? "github:", credentialRefPresent: credential.credentialRef.present, credentialRefSourceKey: credential.credentialRef.key, scope: endpoint === null ? null : "broker-held-github-credential", runnerEnvTokenRequired: false, valuesRead: false, valuesPrinted: false, }; } function brokerNeededResult(options: BrokerAdapterOptions): Record { return { ok: false, failureKind: "auth-missing", degradedReason: "broker-needed", runnerDisposition: "infra-blocked" as RunnerDisposition, retryable: false, brokerNeeded: true, message: "No auth broker endpoint is configured for this dry-run contract; runner env token coverage is reported only for migration diagnostics.", tokenCoverage: runnerEnvTokenCoverage(), brokerCoverage: brokerCoverage(options.endpoint), serviceRegistration: serviceRegistrationContract(), authBroker: { ok: false, source: "broker/auth-broker-needed", capability: "missing-token", nextAction: "configure-auth-broker", runnerEnvTokenRequired: false, brokerIssuedTokenAvailable: false, valuesRead: false, valuesPrinted: false, realPrCreateRequiresCommanderAuthorization: true, }, next: [ "configure UNIDESK_AUTH_BROKER_URL or AUTH_BROKER_URL for broker-backed runner auth", "keep GH_TOKEN/GITHUB_TOKEN out of ordinary runner env once broker mode is enabled", ], redaction: { valuesRead: false, valuesPrinted: false, forbiddenOutputKeys: ["token", "secret", "authorization", "cookie"], }, }; } function auditEventShape(options: BrokerAdapterOptions): Record { return { requestId: "authbroker-contract-request", observedAt: "ISO-8601 timestamp", caller: { plane: "code-queue", taskId: null, queueId: null }, operation: options.operation, repo: options.repo, resource: options.command === "pr-preflight" ? { base: options.base, head: options.head, issueNumber: options.issueNumber } : null, dryRun: true, credentialRef: "github:", credentialKind: "github-rest-token-ref", credentialValuePrinted: false, upstream: { method: "planned", path: "planned GitHub REST path without query secrets" }, status: "HTTP status", ok: "boolean", failureKind: null, degradedReason: null, runnerDisposition: "ready|infra-blocked|business-failed", retryable: "boolean", durationMs: "integer", redaction: { valuesPrinted: false }, }; } function plannedCredentialRequest(options: BrokerAdapterOptions): Record { return { requestId: "authbroker-cli-dry-run", caller: { plane: "manual-cli" }, repo: options.repo, operation: options.operation, dryRun: true, params: options.command === "pr-preflight" ? { base: options.base, head: options.head, issueNumber: options.issueNumber } : {}, }; } function readyContract(options: BrokerAdapterOptions): Record { return { ok: true, runnerDisposition: "ready" as RunnerDisposition, failureKind: null, degradedReason: null, brokerNeeded: false, dryRun: true, mutation: false, capabilities: [...DEFAULT_CAPABILITIES], authBroker: { ok: true, source: "auth-broker", capability: "broker-issued-token", nextAction: "use-auth-broker", runnerEnvTokenRequired: false, brokerIssuedTokenAvailable: true, valuesRead: false, valuesPrinted: false, realPrCreateRequiresCommanderAuthorization: true, }, tokenCoverage: { ok: true, source: "auth-broker", scope: "broker-held-github-credential", runnerEnvTokenRequired: false, valuesRead: false, valuesPrinted: false, }, brokerCoverage: brokerCoverage(options.endpoint), credentialRequest: plannedCredentialRequest(options), auditEventShape: auditEventShape(options), serviceRegistration: serviceRegistrationContract(), prCapabilityContract: { targetBranch: options.base, headBranch: options.head, authSource: "broker-issued-token", systemGhBinaryRequiredForWrites: false, preflightCreatesPr: false, preflightMergesPr: false, realPrCreateRequiresCommanderAuthorization: true, brokerProxy: { ok: true, operations: ["github.auth.status", "github.issue.read", "github.pr.read", "github.pr.create"], writesRemote: false, }, pushDryRun: { runnerLocal: true, coveredByBroker: false, }, }, redaction: { valuesRead: false, valuesPrinted: false, secretKeysBlocked: ["token", "secret", "authorization", "cookie"], }, }; } function contractResult(options: BrokerAdapterOptions): Record { return { ok: true, service: "auth-broker", phase: "p0", commands: [ "bun scripts/cli.ts auth-broker contract", "bun scripts/cli.ts auth-broker health --dry-run", "bun scripts/cli.ts auth-broker credential-request --operation github.pr.create --repo pikasTech/unidesk --dry-run", "bun scripts/cli.ts auth-broker pr-preflight --repo pikasTech/unidesk --base master --head --issue 59 --dry-run", ], capabilities: [...DEFAULT_CAPABILITIES], permissionBoundary: { allowedRepos: [DEFAULT_REPO], liveGithubWrites: false, arbitraryGhApi: false, registryCredentials: false, deployPermissions: false, }, serviceRegistration: serviceRegistrationContract(), runnerNoTokenResult: brokerNeededResult({ ...options, endpoint: null }), readyShape: readyContract({ ...options, endpoint: "" }), }; } export function authBrokerHelp(): unknown { return { command: "auth-broker", output: "json", usage: [ "bun scripts/cli.ts auth-broker contract", "bun scripts/cli.ts auth-broker health --dry-run [--endpoint URL]", "bun scripts/cli.ts auth-broker credential-request --operation github.pr.create --repo pikasTech/unidesk --dry-run [--endpoint URL]", "bun scripts/cli.ts auth-broker pr-preflight --repo pikasTech/unidesk --base master --head [--issue N] --dry-run [--endpoint URL]", ], boundary: [ "P0 adapter is contract/dry-run only and never starts a service.", "GH_TOKEN and GITHUB_TOKEN values are not read or printed; only key presence is reported.", "No dry-run command writes GitHub, registry, deploy, filesystem credential, or service state.", ], reference: "docs/reference/auth-broker.md", }; } export function runAuthBrokerCommand(args: string[]): Record { if (args.some((arg) => arg === "help" || arg === "--help" || arg === "-h")) return authBrokerHelp() as Record; const options = parseOptions(args); if (options.command === "contract") return contractResult(options); const envCoverage = runnerEnvTokenCoverage(); const broker = brokerCoverage(options.endpoint); if (broker.ok !== true) return brokerNeededResult(options); if (options.command === "health") { return { ok: true, dryRun: true, mutation: false, service: "auth-broker", phase: "p0", brokerCoverage: broker, tokenCoverage: envCoverage, serviceRegistration: serviceRegistrationContract(), healthRequest: { method: "GET", path: "/health", wouldCallBroker: false, }, capabilities: [...DEFAULT_CAPABILITIES], redaction: { valuesRead: false, valuesPrinted: false }, }; } return readyContract(options); }