import * as E from "fp-ts/Either" import { pipe } from "fp-ts/function" import { InferredEntity, createVersionedEntity } from "verzod" import { z } from "zod" import V0_VERSION from "./v/0" import V1_VERSION from "./v/1" const versionedObject = z.object({ v: z.number(), }) export const Environment = createVersionedEntity({ latestVersion: 1, versionMap: { 0: V0_VERSION, 1: V1_VERSION, }, getVersion(data) { const versionCheck = versionedObject.safeParse(data) if (versionCheck.success) return versionCheck.data.v // For V0 we have to check the schema const result = V0_VERSION.schema.safeParse(data) return result.success ? 0 : null }, }) export type Environment = InferredEntity export type EnvironmentVariable = InferredEntity< typeof Environment >["variables"][number] const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<>" /** * How much times can we expand environment variables */ const ENV_MAX_EXPAND_LIMIT = 10 /** * Error state when there is a suspected loop while * recursively expanding variables */ const ENV_EXPAND_LOOP = "ENV_EXPAND_LOOP" as const export const EnvironmentSchemaVersion = 1 export function parseBodyEnvVariablesE( body: string, env: Environment["variables"] ) { let result = body let depth = 0 while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) { result = result.replace(REGEX_ENV_VAR, (key) => { const found = env.find( (envVar) => envVar.key === key.replace(/[<>]/g, "") ) if (found && "value" in found) { return found.value } return key }) depth++ } return depth > ENV_MAX_EXPAND_LIMIT ? E.left(ENV_EXPAND_LOOP) : E.right(result) } /** * @deprecated Use `parseBodyEnvVariablesE` instead. */ export const parseBodyEnvVariables = ( body: string, env: Environment["variables"] ) => pipe( parseBodyEnvVariablesE(body, env), E.getOrElse(() => body) ) export function parseTemplateStringE( str: string, variables: | Environment["variables"] | { secret: true; value: string; key: string }[], maskValue = false ) { if (!variables || !str) { return E.right(str) } let result = str let depth = 0 while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) { result = decodeURI(encodeURI(result)).replace(REGEX_ENV_VAR, (_, p1) => { const variable = variables.find((x) => x && x.key === p1) if (variable && "value" in variable) { // Mask the value if it is a secret and explicitly specified if (variable.secret && maskValue) { return "*".repeat( (variable as { secret: true; value: string; key: string }).value .length ) } return variable.value } return "" }) depth++ } return depth > ENV_MAX_EXPAND_LIMIT ? E.left(ENV_EXPAND_LOOP) : E.right(result) } export type NonSecretEnvironmentVariable = Extract< EnvironmentVariable, { secret: false } > export type NonSecretEnvironment = Omit & { variables: NonSecretEnvironmentVariable[] } /** * @deprecated Use `parseTemplateStringE` instead */ export const parseTemplateString = ( str: string, variables: | Environment["variables"] | { secret: true; value: string; key: string }[], maskValue = false ) => pipe( parseTemplateStringE(str, variables, maskValue), E.getOrElse(() => str) ) export const translateToNewEnvironmentVariables = ( x: any ): Environment["variables"][number] => { return { key: x.key, value: x.value, secret: false, } } export const translateToNewEnvironment = (x: any): Environment => { if (x.v && x.v === EnvironmentSchemaVersion) return x // Legacy const id = x.id ?? "" const name = x.name ?? "Untitled" const variables = (x.variables ?? []).map(translateToNewEnvironmentVariables) return { v: EnvironmentSchemaVersion, id, name, variables, } }