pi-mono/apps/p2/plugins/models/config-value.ts
Mario Zechner 05dc8d239d feat: add apps/p2 (imported from pi-private at 7dadff697)
p2 (Pi Mobile on Chord v2) moves from pi-private into pi as one commit. Root scripts that loop all workspaces
use --if-present; npm test, ./test.sh and the release script cover packages/* only (p2: cd apps/p2 && npm test);
sync-versions also updates apps' workspace pins.
2026-11-03 13:39:06 +00:00

192 lines
5.9 KiB
TypeScript

// Adapted from earendil-works/pi (MIT); see third-party/pi.LICENSE.
/**
* Resolve configuration values that may be shell commands, environment variables, or literals.
* Used by auth-storage.ts and model-registry.ts.
*/
import { execSync } from "node:child_process";
import { processLogs } from "@p2/app/v1/node";
const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
const ENV_VAR_NAME_PREFIX_RE = /^[A-Za-z_][A-Za-z0-9_]*/;
type TemplatePart = { type: "literal"; value: string } | { type: "env"; name: string };
type ConfigValueReference = { type: "command"; config: string } | { type: "template"; parts: TemplatePart[] };
function appendLiteral(parts: TemplatePart[], value: string): void {
if (!value) return;
const previousPart = parts[parts.length - 1];
if (previousPart?.type === "literal") {
previousPart.value += value;
return;
}
parts.push({ type: "literal", value });
}
function parseConfigValueTemplate(config: string): TemplatePart[] {
const parts: TemplatePart[] = [];
let index = 0;
while (index < config.length) {
const dollarIndex = config.indexOf("$", index);
if (dollarIndex < 0) {
appendLiteral(parts, config.slice(index));
break;
}
appendLiteral(parts, config.slice(index, dollarIndex));
const nextChar = config[dollarIndex + 1];
if (nextChar === "$" || nextChar === "!") {
appendLiteral(parts, nextChar);
index = dollarIndex + 2;
continue;
}
if (nextChar === "{") {
const endIndex = config.indexOf("}", dollarIndex + 2);
if (endIndex < 0) {
appendLiteral(parts, "$");
index = dollarIndex + 1;
continue;
}
const name = config.slice(dollarIndex + 2, endIndex);
if (ENV_VAR_NAME_RE.test(name)) {
parts.push({ type: "env", name });
} else {
appendLiteral(parts, config.slice(dollarIndex, endIndex + 1));
}
index = endIndex + 1;
continue;
}
const match = config.slice(dollarIndex + 1).match(ENV_VAR_NAME_PREFIX_RE);
if (match) {
parts.push({ type: "env", name: match[0] });
index = dollarIndex + 1 + match[0].length;
continue;
}
appendLiteral(parts, "$");
index = dollarIndex + 1;
}
return parts;
}
function parseConfigValueReference(config: string): ConfigValueReference {
if (config.startsWith("!")) {
return { type: "command", config };
}
return { type: "template", parts: parseConfigValueTemplate(config) };
}
function resolveEnvConfigValue(name: string, env?: Record<string, string>): string | undefined {
return env?.[name] || process.env[name] || undefined;
}
function getTemplateEnvVarNames(parts: TemplatePart[]): string[] {
const names: string[] = [];
for (const part of parts) {
if (part.type !== "env" || names.includes(part.name)) continue;
names.push(part.name);
}
return names;
}
function resolveTemplate(parts: TemplatePart[], env?: Record<string, string>): string | undefined {
let resolved = "";
for (const part of parts) {
if (part.type === "literal") {
resolved += part.value;
continue;
}
const envValue = resolveEnvConfigValue(part.name, env);
if (envValue === undefined) return undefined;
resolved += envValue;
}
return resolved;
}
export function getConfigValueEnvVarNames(config: string): string[] {
const reference = parseConfigValueReference(config);
return reference.type === "template" ? getTemplateEnvVarNames(reference.parts) : [];
}
function getMissingConfigValueEnvVarNames(config: string, env?: Record<string, string>): string[] {
return getConfigValueEnvVarNames(config).filter((name) => resolveEnvConfigValue(name, env) === undefined);
}
export function isCommandConfigValue(config: string): boolean {
return parseConfigValueReference(config).type === "command";
}
function executeWithDefaultShell(command: string): string | undefined {
try {
const output = execSync(command, {
encoding: "utf-8",
shell: process.env.SHELL,
timeout: 10000,
stdio: ["ignore", "pipe", "ignore"],
});
return output.trim() || undefined;
} catch {
return undefined;
}
}
function executeCommandUncached(commandConfig: string): string | undefined {
return executeWithDefaultShell(commandConfig.slice(1));
}
/**
* Resolve all header values using the same resolution logic as API keys.
*/
export function resolveConfigValueUncached(config: string, env?: Record<string, string>): string | undefined {
const reference = parseConfigValueReference(config);
const resolved =
reference.type === "command" ? executeCommandUncached(reference.config) : resolveTemplate(reference.parts, env);
// Protect actual values, not only their environment/template/command references in the config files.
if (resolved !== undefined) processLogs.redactor.addSecret(resolved);
return resolved;
}
export function resolveConfigValueOrThrow(config: string, description: string, env?: Record<string, string>): string {
const resolvedValue = resolveConfigValueUncached(config, env);
if (resolvedValue !== undefined) {
return resolvedValue;
}
const reference = parseConfigValueReference(config);
if (reference.type === "command") {
throw new Error(`Failed to resolve ${description} from shell command`);
}
if (reference.type === "template") {
const missingEnvVars = getMissingConfigValueEnvVarNames(config, env);
if (missingEnvVars.length === 1) {
throw new Error(`Failed to resolve ${description} from environment variable: ${missingEnvVars[0]}`);
}
if (missingEnvVars.length > 1) {
throw new Error(`Failed to resolve ${description} from environment variables: ${missingEnvVars.join(", ")}`);
}
}
throw new Error(`Failed to resolve ${description}`);
}
export function resolveHeadersOrThrow(
headers: Record<string, string> | undefined,
description: string,
env?: Record<string, string>,
): Record<string, string> | undefined {
if (!headers) return undefined;
const resolved: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
resolved[key] = resolveConfigValueOrThrow(value, `${description} header "${key}"`, env);
}
return Object.keys(resolved).length > 0 ? resolved : undefined;
}