import { Redacted } from "effect"; import { isEnvReference, ENV_CAPTURE_REGEX, ENV_CAPTURE_REGEX_STRICT } from "./env.ts"; import { isSecretPath } from "./secret-paths.ts"; import type { CliProjectEnvironment } from "../project.ts"; type ResolvedString = string | Redacted.Redacted; export type ResolvedCliConfigValue = T extends string ? ResolvedString : T extends ReadonlyArray ? ReadonlyArray> : T extends Array ? Array> : T extends Record ? { readonly [K in keyof T]: ResolvedCliConfigValue } & { readonly [key: string]: ResolvedCliConfigValue; } : T extends object ? { readonly [K in keyof T]: ResolvedCliConfigValue } : T; export function toPathSegments(path: string): ReadonlyArray { if (path === "") { return []; } return path.split(".").filter((segment) => segment.length > 0); } function interpolateLeafValue( value: string, env: Readonly>, goViperCompat: boolean, ): string { const match = (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); const envName = match?.[1]; if (envName === undefined) { return value; } const resolved = env[envName]; // Preserve the literal `env(VAR)` verbatim when VAR is unset OR present but // empty (e.g. a dotenv `KEY=` line), the same gate as `substituteEnvLeaf` in // `./env.ts`. Without this, a present-but-empty `env(...)` secret (e.g. // `edge_runtime.secrets.FOO = "env(EMPTY)"`) resolves to `""` here, gets // redacted by `redactValue` as a real value instead of skipped as an // unresolved literal, and `secrets set` uploads a blank secret Go would // never send. if (resolved === undefined || resolved === "") { return value; } return resolved; } function interpolateValue( value: unknown, env: Readonly>, goViperCompat: boolean, ): unknown { if (Array.isArray(value)) { return value.map((item) => interpolateValue(item, env, goViperCompat)); } if (typeof value === "object" && value !== null) { const result: Record = {}; for (const [key, child] of Object.entries(value)) { result[key] = interpolateValue(child, env, goViperCompat); } return result; } if (typeof value === "string") { return interpolateLeafValue(value, env, goViperCompat); } return value; } function redactValue(value: unknown, path: ReadonlyArray, goViperCompat: boolean): unknown { if (Array.isArray(value)) { return value.map((item, index) => redactValue(item, [...path, String(index)], goViperCompat)); } if (typeof value === "object" && value !== null) { const result: Record = {}; for (const [key, child] of Object.entries(value)) { result[key] = redactValue(child, [...path, key], goViperCompat); } return result; } if (typeof value === "string" && isSecretPath(path) && !isEnvReference(value, goViperCompat)) { return Redacted.make(value, { label: path.join(".") }); } return value; } /** * Shared by the plain sync resolvers below and the Effect-typed variants in `../project.ts`. * Declared as an overload pair so the `unknown`-typed implementation flows through to * `interpolateValue`/`redactValue` without an `as` cast. */ export function resolveCliConfigValueAtPath( value: T, cliProjectEnv: Pick, path: ReadonlyArray, goViperCompat: boolean, ): ResolvedCliConfigValue; export function resolveCliConfigValueAtPath( value: unknown, cliProjectEnv: Pick, path: ReadonlyArray, goViperCompat: boolean, ): unknown { const interpolated = interpolateValue(value, cliProjectEnv.values, goViperCompat); return redactValue(interpolated, path, goViperCompat); } /** * Plain synchronous counterpart to the Effect-typed `resolveCliConfigValue` in `../project.ts`; * has no options parameter, since this package's only resolver knob (`goViperCompat`) is * internal-only. */ export function resolveCliConfigValue( value: T, cliProjectEnv: Pick, configPath: string, ): ResolvedCliConfigValue { return resolveCliConfigValueAtPath(value, cliProjectEnv, toPathSegments(configPath), false); } /** See {@link resolveCliConfigValue}. */ export function resolveCliConfigSubtree( value: T, cliProjectEnv: Pick, pathPrefix: string, ): ResolvedCliConfigValue { return resolveCliConfigValueAtPath(value, cliProjectEnv, toPathSegments(pathPrefix), false); }