From 3c769062c3f6b9a8b7d8de2921bdd47dbb686ca3 Mon Sep 17 00:00:00 2001 From: edow <285729101@qq.com> Date: Wed, 18 Feb 2026 13:56:55 +0800 Subject: [PATCH 1/2] feat: auto-register template creator as referrer in echo-start When scaffolding from an external template, echo-start now reads echo.config.json for a referralCode and writes it to .env.local so the OAuth flow auto-registers the template creator as referrer. Closes #612 --- packages/sdk/echo-start/src/index.ts | 103 ++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index c249d50d1..d2aede6e4 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -14,7 +14,7 @@ import chalk from 'chalk'; import { spawn } from 'child_process'; import { Command } from 'commander'; import degit from 'degit'; -import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import path from 'path'; const program = new Command(); @@ -202,6 +202,68 @@ function resolveTemplateRepo(template: string): string { return repo; } +interface EchoTemplateConfig { + referralCode?: string; +} + +/** + * Read echo.config.json from a scaffolded template directory. + * Template creators can include this file to specify their referral code, + * which will be auto-registered when a user scaffolds from the template. + */ +function readTemplateReferralConfig( + projectPath: string +): EchoTemplateConfig | null { + const configPath = path.join(projectPath, 'echo.config.json'); + if (!existsSync(configPath)) { + return null; + } + + try { + const content = readFileSync(configPath, 'utf-8'); + const config = JSON.parse(content) as EchoTemplateConfig; + return config; + } catch { + return null; + } +} + +/** + * Extract the GitHub owner (user or org) from a GitHub template URL. + * e.g. "https://github.com/someuser/my-template" -> "someuser" + */ +function extractTemplateOwner(templateUrl: string): string | null { + const repo = resolveTemplateRepo(templateUrl); + const parts = repo.split('/'); + return parts.length >= 2 ? parts[0] : null; +} + +/** + * Detect the appropriate env var name for the referral code based on the project's framework. + */ +function detectReferralEnvVarName(projectPath: string): string { + const pkgPath = path.join(projectPath, 'package.json'); + + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + const deps = { ...pkg.dependencies, ...pkg.devDependencies }; + + if (deps['next']) { + return 'NEXT_PUBLIC_ECHO_REFERRAL_CODE'; + } else if (deps['vite']) { + return 'VITE_ECHO_REFERRAL_CODE'; + } else if (deps['react-scripts']) { + return 'REACT_APP_ECHO_REFERRAL_CODE'; + } + } catch { + // Fall through to default + } + } + + return 'NEXT_PUBLIC_ECHO_REFERRAL_CODE'; +} + function detectEnvVarName(projectPath: string): string | null { const envFiles = ['.env.local', '.env.example', '.env']; @@ -414,6 +476,45 @@ async function createApp(projectDir: string, options: CreateAppOptions) { log.message(`Created .env.local with ${envVarName}`); } + // Handle template referral system for external templates. + // If the template includes an echo.config.json with a referralCode, + // write it to .env.local so the OAuth flow auto-registers the template + // creator as the referrer on the new app. + // See: https://echo.merit.systems/docs/money/referrals + if (isExternal) { + const templateConfig = readTemplateReferralConfig(absoluteProjectPath); + const referralCode = templateConfig?.referralCode; + + if (referralCode) { + const referralEnvVar = detectReferralEnvVarName(absoluteProjectPath); + const envFilePath = path.join(absoluteProjectPath, '.env.local'); + + if (existsSync(envFilePath)) { + const currentEnv = readFileSync(envFilePath, 'utf-8'); + writeFileSync( + envFilePath, + currentEnv.trimEnd() + `\n${referralEnvVar}=${referralCode}\n` + ); + } else { + writeFileSync(envFilePath, `${referralEnvVar}=${referralCode}\n`); + } + + const owner = extractTemplateOwner(template); + log.message( + `Registered template referral code from ${owner ? `@${owner}` : 'template creator'}` + ); + } + + // Clean up echo.config.json - it's template metadata, not app code + const echoConfigPath = path.join( + absoluteProjectPath, + 'echo.config.json' + ); + if (existsSync(echoConfigPath)) { + unlinkSync(echoConfigPath); + } + } + log.step('Project setup completed successfully'); // Auto-install dependencies unless skipped From 171086522e1740b001edb41ee103d1153d289d3d Mon Sep 17 00:00:00 2001 From: edow <285729101@qq.com> Date: Wed, 18 Feb 2026 15:06:46 +0800 Subject: [PATCH 2/2] fix: sanitize referral code to prevent env variable injection Add sanitizeReferralCode() that validates referral codes from echo.config.json against a strict alphanumeric allowlist pattern before writing to .env.local. Malicious template authors could previously inject arbitrary environment variables via newlines or special characters in the referralCode field. --- packages/sdk/echo-start/src/index.ts | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index d2aede6e4..4c2ecc01b 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -228,6 +228,31 @@ function readTemplateReferralConfig( } } +/** + * Sanitize a referral code to prevent environment variable injection. + * Only allows alphanumeric characters, hyphens, underscores, and dots. + * Returns null if the code is invalid or empty after sanitization. + */ +function sanitizeReferralCode(code: unknown): string | null { + if (typeof code !== 'string' || code.length === 0) { + return null; + } + + // Strict allowlist: only alphanumeric, hyphens, underscores, dots + const SAFE_REFERRAL_PATTERN = /^[a-zA-Z0-9_\-\.]+$/; + + if (!SAFE_REFERRAL_PATTERN.test(code)) { + return null; + } + + // Enforce a reasonable max length + if (code.length > 128) { + return null; + } + + return code; +} + /** * Extract the GitHub owner (user or org) from a GitHub template URL. * e.g. "https://github.com/someuser/my-template" -> "someuser" @@ -483,7 +508,14 @@ async function createApp(projectDir: string, options: CreateAppOptions) { // See: https://echo.merit.systems/docs/money/referrals if (isExternal) { const templateConfig = readTemplateReferralConfig(absoluteProjectPath); - const referralCode = templateConfig?.referralCode; + const referralCode = sanitizeReferralCode(templateConfig?.referralCode); + + if (templateConfig?.referralCode && !referralCode) { + log.warning( + 'Template referral code was ignored: contains invalid characters. ' + + 'Only alphanumeric characters, hyphens, underscores, and dots are allowed.' + ); + } if (referralCode) { const referralEnvVar = detectReferralEnvVarName(absoluteProjectPath);