mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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.
192 lines
5.9 KiB
TypeScript
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;
|
|
}
|