import { validateWorkspaceDescriptor } from "./schema.js"; import type { DwhTransport, WorkspaceDescriptor } from "./schema.js"; export type InstallationRole = "DWH" | "EVIDENCE"; export type InstallationSuffix = | "TRANSPORT" | "HOST" | "PORT" | "BASE_URL" | "USER" | "PASSWORD_FILE" | "API_KEY_FILE" | "TLS_CA_FILE" | "SSH_HOST" | "SSH_PORT" | "SSH_USER" | "SSH_PRIVATE_KEY_FILE" | "SSH_KNOWN_HOSTS_FILE" | "SSH_TARGET_HOST" | "SSH_TARGET_PORT" | "SIGNED_URLS_FILE" | "ACCESS_KEY_FILE" | "SECRET_KEY_FILE" | "SESSION_TOKEN_FILE"; type ConnectorTransport = DwhTransport; export interface InstallationVariable { name: string; role: InstallationRole; suffix: InstallationSuffix; secret: boolean; transports?: readonly ConnectorTransport[]; } export interface InstallationContract { workspaceId: string; namespace: string; variables: InstallationVariable[]; } const DIRECT_SUFFIXES: readonly InstallationSuffix[] = [ "HOST", "PORT", "USER", "PASSWORD_FILE", "TLS_CA_FILE", ]; const REST_SUFFIXES: readonly InstallationSuffix[] = [ "BASE_URL", "API_KEY_FILE", "TLS_CA_FILE", ]; const SSH_SUFFIXES: readonly InstallationSuffix[] = [ "USER", "PASSWORD_FILE", "TLS_CA_FILE", "SSH_HOST", "SSH_PORT", "SSH_USER", "SSH_PRIVATE_KEY_FILE", "SSH_KNOWN_HOSTS_FILE", "SSH_TARGET_HOST", "SSH_TARGET_PORT", ]; function namespaceFor(workspace: WorkspaceDescriptor): string { return workspace.workspace.id.replaceAll("-", "_").toUpperCase(); } function createVariable( namespace: string, role: InstallationRole, suffix: InstallationSuffix, transports?: readonly ConnectorTransport[], ): InstallationVariable { return { name: `THT_WS_${namespace}_${role}_${suffix}`, role, suffix, secret: suffix.endsWith("_FILE"), ...(transports ? { transports } : {}), }; } function connectorVariables( namespace: string, transports: readonly DwhTransport[], ): InstallationVariable[] { const suffixTransports = new Map(); const add = (suffixes: readonly InstallationSuffix[], transport: DwhTransport) => { for (const suffix of suffixes) { const applicable = suffixTransports.get(suffix) ?? []; applicable.push(transport); suffixTransports.set(suffix, applicable); } }; for (const transport of transports) { if (transport === "postgres_direct") { add(DIRECT_SUFFIXES, transport); } else if (transport === "rest_api") { add(REST_SUFFIXES, transport); } else { add(SSH_SUFFIXES, transport); } } return [ createVariable(namespace, "DWH", "TRANSPORT", transports), ...[...suffixTransports.entries()].map(([suffix, applicable]) => ( createVariable(namespace, "DWH", suffix, applicable) )), ]; } function evidenceVariables( namespace: string, workspace: WorkspaceDescriptor, ): InstallationVariable[] { if (!("evidence" in workspace) || workspace.evidence === undefined) return []; const source = workspace.evidence.source; if (source.type === "http" && source.authentication === "signed_urls_file") { return [createVariable(namespace, "EVIDENCE", "SIGNED_URLS_FILE")]; } if (source.type === "s3" && source.credentials === "static_files") { return [ createVariable(namespace, "EVIDENCE", "ACCESS_KEY_FILE"), createVariable(namespace, "EVIDENCE", "SECRET_KEY_FILE"), createVariable(namespace, "EVIDENCE", "SESSION_TOKEN_FILE"), ]; } return []; } function requireSupportedDescriptor(workspace: unknown): void { if (typeof workspace !== "object" || workspace === null) { throw new Error("Installation contract supports only workspace schema version 4"); } const metadata = Reflect.get(workspace, "workspace"); if (typeof metadata !== "object" || metadata === null || Reflect.get(metadata, "schema_version") !== 4) { throw new Error("Installation contract supports only workspace schema version 4"); } } export function buildInstallationContract(workspace: WorkspaceDescriptor): InstallationContract { requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); const namespace = namespaceFor(descriptor); return { workspaceId: descriptor.workspace.id, namespace, variables: [ ...(descriptor.dwh ? connectorVariables(namespace, descriptor.dwh.supported_transports) : []), ...evidenceVariables(namespace, descriptor), ], }; } function localizedIntroduction(workspace: WorkspaceDescriptor): string { return workspace.workspace.language === "it" ? `Configurazione dell'installazione per ${workspace.workspace.name}. Imposta solo i binding supportati da questa installazione.` : `Installation setup for ${workspace.workspace.name}. Configure only the bindings supported by this installation.`; } function evidenceDocumentation( workspace: WorkspaceDescriptor, variables: readonly InstallationVariable[], ): string[] { if (!("evidence" in workspace) || workspace.evidence === undefined) return []; const { source, policy } = workspace.evidence; const common = [ "## Evidence source", "", `- Type: \`${source.type}\``, ]; let details: string[]; if (source.type === "filesystem") { details = [ `- URI: \`${source.uri}\``, `- Patterns: ${source.patterns.map((pattern) => `\`${pattern}\``).join(", ")}`, `- Maximum source bytes: \`${source.max_bytes}\``, "- Ownership: the descriptor and its Evidence tree are owned by the same Git revision.", "- Materialization: P6 materializes that revision-pinned tree and verifies real containment, including symlink safety.", "- Export boundary: the browser/API ZIP does not include Evidence file bytes.", ]; } else if (source.type === "http") { details = [ "- URIs:", ...source.uris.map((uri) => ` - \`${uri}\``), `- Authentication: \`${source.authentication}\`. ${source.authentication === "none" ? "No credential file is required." : "Provide the signed URL file through the installation file variable listed below."}`, `- Connect timeout (ms): \`${source.connect_timeout_ms}\``, `- Read timeout (ms): \`${source.read_timeout_ms}\``, `- Maximum source bytes: \`${source.max_bytes}\``, `- Maximum redirects: \`${source.max_redirects}\``, `- Private hosts allowed: \`${source.allow_private_hosts}\``, `- Maximum cache bytes: \`${source.max_cache_bytes}\``, ]; } else { details = [ `- URI: \`${source.uri}\``, ...(source.endpoint_url === undefined ? [] : [`- Endpoint URL: \`${source.endpoint_url}\``]), ...(source.region === undefined ? [] : [`- Region: \`${source.region}\``]), `- Credentials: \`${source.credentials}\`. ${source.credentials === "ambient" ? "Use ambient credentials; no Evidence credential file is required." : "Provide credentials through the installation file variables listed below."}`, `- Trusted endpoint: \`${source.trusted_endpoint}\``, `- Private endpoint allowed: \`${source.allow_private_endpoint}\``, `- Insecure endpoint allowed: \`${source.allow_insecure_endpoint}\``, `- Maximum source bytes: \`${source.max_bytes}\``, `- Maximum objects: \`${source.max_objects}\``, `- Maximum pages: \`${source.max_pages}\``, `- Page size: \`${source.page_size}\``, ]; } const evidenceVariables = variables.filter(({ role }) => role === "EVIDENCE"); const requiredVariables = evidenceVariables.filter(({ suffix }) => suffix !== "SESSION_TOKEN_FILE"); const optionalVariables = evidenceVariables.filter(({ suffix }) => suffix === "SESSION_TOKEN_FILE"); return [ ...common, ...details, `- Maximum chunk characters: \`${policy.max_chunk_chars}\``, `- Retained published generations: \`${policy.retain_published_generations}\``, ...(requiredVariables.length === 0 ? [] : [ "- Required installation file variables:", ...requiredVariables.map(({ name }) => ` - \`${name}\``), ]), ...(optionalVariables.length === 0 ? [] : [ "- Optional installation file variables:", ...optionalVariables.map(({ name }) => ` - \`${name}\``), ]), "", ]; } export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExample: string; markdown: string } { const descriptor = validateWorkspaceDescriptor(workspace); const contract = buildInstallationContract(descriptor); const variablesByRole = new Map(); for (const variable of contract.variables) { const variables = variablesByRole.get(variable.role) ?? []; variables.push(variable); variablesByRole.set(variable.role, variables); } const envExample = [ `# Generated installation bindings for ${descriptor.workspace.id}`, "# Provide secret file paths only; never paste secret values here.", ...contract.variables.map((variable) => `${variable.name}=`), "", ].join("\n"); const markdown = [ "# Installation requirements", "", `**Workspace:** ${descriptor.workspace.name}`, "", localizedIntroduction(descriptor), "", "Use the following UI fields as installation bindings. Secret fields always contain file paths, never secret values.", "", ...(["DWH", "EVIDENCE"] as const) .filter((role) => variablesByRole.has(role)) .flatMap((role) => [ `## ${role === "DWH" ? "Data warehouse" : "Evidence"}`, "", ...(variablesByRole.get(role) ?? []).map((variable) => ( `- \`${variable.name}\`${variable.transports ? ` (for: ${variable.transports.join(", ")})` : ""}` )), "", ]), ...evidenceDocumentation(descriptor, contract.variables), ].join("\n"); return { envExample, markdown }; }