diff --git a/src/App.tsx b/src/App.tsx index e717345..253a2c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -329,10 +329,26 @@ function inferDefaultValue(key: string, value: string): string | null { const isEmpty = value.length === 0; const shouldFillGeneric = hasPlaceholder || isEmpty; + if (signal === "APP_ENV" || signal === "RAILS_ENV") { + return "production"; + } + if (signal === "NODE_ENV") { return "production"; } + if (signal.includes("LOG_LEVEL")) { + return "info"; + } + + if (signal.includes("TELEMETRY_ENABLED") || signal.includes("PROMETHEUS_METRICS") || signal.includes("METRICS_ENABLED")) { + return "false"; + } + + if (signal.includes("CORS_CREDENTIALS") || signal.includes("USE_SSL") || signal.includes("REQUIRE_SSL")) { + return "true"; + } + if (signal.includes("BOOTSTRAP_ADMIN_EMAIL") || signal.includes("ADMIN_EMAIL")) { return "admin@example.local"; } @@ -349,10 +365,50 @@ function inferDefaultValue(key: string, value: string): string | null { return "admin@example.local"; } - if (shouldFillGeneric && (signal.includes("BASE_URL") || signal.endsWith("_URL") || signal.endsWith("_URI") || signal.includes("ORIGIN"))) { + if (shouldFillGeneric && (signal.includes("BASE_URL") || signal.endsWith("_URL") || signal.endsWith("_URI") || signal.includes("ORIGIN") || signal.includes("SITE_URL"))) { return "https://example.local"; } + if (shouldFillGeneric && signal.includes("CORS_ORIGIN")) { + return "https://example.local"; + } + + if (shouldFillGeneric && signal.includes("SMTP_HOST")) { + return "smtp.example.local"; + } + + if (shouldFillGeneric && signal.includes("SMTP_PORT")) { + return "587"; + } + + if (shouldFillGeneric && signal.includes("SMTP_USER")) { + return "smtp-user"; + } + + if (shouldFillGeneric && (signal.includes("SMTP_FROM") || signal.includes("MAIL_FROM") || signal.includes("SENDER_EMAIL"))) { + return "noreply@example.local"; + } + + if (shouldFillGeneric && (signal.includes("S3_BUCKET") || signal.includes("BUCKET_NAME"))) { + return "app-bucket"; + } + + if (shouldFillGeneric && (signal.includes("S3_REGION") || signal.includes("AWS_REGION") || signal.includes("SQS_REGION"))) { + return "eu-central-1"; + } + + if (shouldFillGeneric && signal.includes("S3_ENDPOINT")) { + return "https://s3.example.local"; + } + + if (shouldFillGeneric && signal.includes("RABBITMQ_URI")) { + return "amqp://rabbitmq:5672"; + } + + if (shouldFillGeneric && signal.includes("REDIS_URL")) { + return "redis://redis:6379/0"; + } + if (shouldFillGeneric && signal.includes("PORT")) { if (signal.includes("POSTGRES")) { return "5432"; @@ -362,6 +418,14 @@ function inferDefaultValue(key: string, value: string): string | null { return "6379"; } + if (signal.includes("SMTP")) { + return "587"; + } + + if (signal.includes("S3") || signal.includes("MINIO")) { + return "9000"; + } + return "3000"; } diff --git a/src/env.ts b/src/env.ts index e2e876f..4328fae 100644 --- a/src/env.ts +++ b/src/env.ts @@ -145,6 +145,8 @@ type Requirement = | { type: "integer"; min: number; max: number; label: string } | { type: "url"; label: string } | { type: "email"; label: string } + | { type: "external"; value: string; label: string } + | { type: "prefixed"; prefix: string; length: number; alphabet: string; label: string } | { type: "secret"; length: number; label: string } | { type: "password"; length: number; urlSafe: boolean; label: string }; @@ -179,10 +181,84 @@ function inferRequirement(placeholder: string, key: string): Requirement { return { type: "integer", min: 1024, max: 49151, label: "TCP port number" }; } + if (includesAny(combinedSignal, ["BOOLEAN", "BOOL", "ENABLED", "DISABLED", "USE_SSL", "REQUIRE_SSL"])) { + return { type: "external", value: "false", label: "boolean flag" }; + } + if (placeholderSignal.includes("EMAIL") || keySignal.includes("EMAIL")) { return { type: "email", label: "email address" }; } + if (includesAny(combinedSignal, ["STRIPE_WEBHOOK_SECRET", "WEBHOOK_SECRET"])) { + return { + type: "prefixed", + prefix: "whsec_", + length: 32, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + label: "Stripe-style webhook secret" + }; + } + + if (includesAny(combinedSignal, ["STRIPE_SECRET_KEY"])) { + return { + type: "prefixed", + prefix: "sk_test_", + length: 32, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + label: "Stripe-style secret key" + }; + } + + if (includesAny(combinedSignal, ["STRIPE_PUBLISHABLE_KEY", "PUBLISHABLE_KEY"])) { + return { + type: "prefixed", + prefix: "pk_test_", + length: 32, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + label: "Stripe-style publishable key" + }; + } + + if (includesAny(combinedSignal, ["RESEND_API_KEY"])) { + return { + type: "prefixed", + prefix: "re_", + length: 24, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + label: "Resend-style API key" + }; + } + + if (includesAny(combinedSignal, ["OPENAI_API_KEY"])) { + return { + type: "prefixed", + prefix: "sk-", + length: 48, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + label: "OpenAI-style API key placeholder" + }; + } + + if (includesAny(combinedSignal, ["ANTHROPIC_API_KEY"])) { + return { + type: "prefixed", + prefix: "sk-ant-", + length: 42, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + label: "Anthropic-style API key placeholder" + }; + } + + if (includesAny(combinedSignal, ["AWS_ACCESS_KEY_ID", "S3_ACCESS_KEY", "ACCESS_KEY_ID"])) { + return { + type: "prefixed", + prefix: "AKIA", + length: 16, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + label: "AWS/S3 access key ID format" + }; + } + if (placeholderSignal.includes("PASSWORD") || keySignal.includes("PASSWORD")) { const length = placeholderSignal.includes("LONG") || keySignal.includes("ADMIN") ? 28 : 24; return { @@ -193,6 +269,30 @@ function inferRequirement(placeholder: string, key: string): Requirement { }; } + if (includesAny(combinedSignal, ["SMTP_HOST"])) { + return { type: "external", value: "smtp.example.local", label: "SMTP host" }; + } + + if (includesAny(combinedSignal, ["SMTP_USER"])) { + return { type: "external", value: "smtp-user", label: "SMTP username" }; + } + + if (includesAny(combinedSignal, ["S3_BUCKET", "BUCKET_NAME"])) { + return { type: "external", value: "app-bucket", label: "S3 bucket name" }; + } + + if (includesAny(combinedSignal, ["S3_REGION", "AWS_REGION", "SQS_REGION"])) { + return { type: "external", value: "eu-central-1", label: "cloud region" }; + } + + if (includesAny(combinedSignal, ["LOG_LEVEL"])) { + return { type: "external", value: "info", label: "log level" }; + } + + if (includesAny(combinedSignal, ["CORS_ORIGIN", "IFRAME_ANCESTORS", "ALLOWED_ORIGINS", "ALLOWED_HOSTS"])) { + return { type: "url", label: "allowed HTTPS origin" }; + } + if (placeholderSignal.includes("URL") || placeholderSignal.includes("URI") || placeholderSignal.includes("ORIGIN")) { return { type: "url", label: "HTTPS URL" }; } @@ -234,6 +334,10 @@ function generateValue(requirement: Requirement): string { return "https://example.local"; case "email": return "admin@example.local"; + case "external": + return requirement.value; + case "prefixed": + return `${requirement.prefix}${randomString(requirement.length, requirement.alphabet)}`; case "password": return randomString( requirement.length, @@ -273,6 +377,10 @@ function bytesToBase64(bytes: Uint8Array): string { return btoa(binary); } +function includesAny(signal: string, needles: string[]): boolean { + return needles.some((needle) => signal.includes(needle)); +} + function extractNumberBefore(signal: string, marker: string): number | null { const match = signal.match(new RegExp(`(\\d+)_${marker}`)); return match ? Number(match[1]) : null;