fix: verify vercel domain after deployment

This commit is contained in:
lucas-neynar 2025-03-21 12:23:51 -07:00
parent 6128212f0d
commit 1d55fbe940
No known key found for this signature in database
3 changed files with 118 additions and 133 deletions

View File

@ -176,12 +176,14 @@ async function init() {
break; break;
} }
const defaultFrameName = neynarAppName.toLowerCase().includes('demo') ? undefined : neynarAppName;
const answers = await inquirer.prompt([ const answers = await inquirer.prompt([
{ {
type: 'input', type: 'input',
name: 'projectName', name: 'projectName',
message: 'What is the name of your frame?', message: '⚠️ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\nWhat is the name of your frame?',
default: neynarAppName || undefined, default: defaultFrameName,
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Project name cannot be empty'; return 'Project name cannot be empty';
@ -211,18 +213,6 @@ async function init() {
} }
return true; return true;
} }
},
{
type: 'input',
name: 'splashImageUrl',
message: 'Enter the URL for your splash image\n(optional -- leave blank to use the default public/splash.png image or replace public/splash.png with your own)\n\nExternal splash image URL:',
default: neynarAppLogoUrl || undefined
},
{
type: 'input',
name: 'iconImageUrl',
message: 'Enter the URL for your app icon\n(optional -- leave blank to use the default public/icon.png image or replace public/icon.png with your own)\n\nExternal app icon URL:',
default: neynarAppLogoUrl || undefined
} }
]); ]);
@ -242,7 +232,7 @@ async function init() {
'- Cannot test frame embeds or mobile devices\n\n' + '- Cannot test frame embeds or mobile devices\n\n' +
'Note: You can always switch between localhost and tunnel by editing the USE_TUNNEL environment variable in .env.local\n\n' + 'Note: You can always switch between localhost and tunnel by editing the USE_TUNNEL environment variable in .env.local\n\n' +
'Use tunnel?', 'Use tunnel?',
default: true default: false
} }
]); ]);
answers.useTunnel = hostingAnswer.useTunnel; answers.useTunnel = hostingAnswer.useTunnel;
@ -423,14 +413,6 @@ async function init() {
fs.appendFileSync(envPath, `\nFID="${fid}"`); fs.appendFileSync(envPath, `\nFID="${fid}"`);
} }
if (answers.splashImageUrl) {
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL="${answers.splashImageUrl}"`);
}
if (answers.iconImageUrl) {
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_ICON_IMAGE_URL="${answers.iconImageUrl}"`);
}
// Append all remaining environment variables // Append all remaining environment variables
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`);

View File

@ -1,6 +1,6 @@
{ {
"name": "create-neynar-farcaster-frame", "name": "create-neynar-farcaster-frame",
"version": "1.1.3", "version": "1.1.4",
"type": "module", "type": "module",
"files": [ "files": [
"bin/index.js" "bin/index.js"

View File

@ -25,6 +25,7 @@ async function validateSeedPhrase(seedPhrase) {
} }
async function generateFarcasterMetadata(domain, accountAddress, seedPhrase, webhookUrl) { async function generateFarcasterMetadata(domain, accountAddress, seedPhrase, webhookUrl) {
const trimmedDomain = domain.trim();
const header = { const header = {
type: 'custody', type: 'custody',
key: accountAddress, key: accountAddress,
@ -32,7 +33,7 @@ async function generateFarcasterMetadata(domain, accountAddress, seedPhrase, web
const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64');
const payload = { const payload = {
domain domain: trimmedDomain
}; };
const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url');
@ -51,11 +52,11 @@ async function generateFarcasterMetadata(domain, accountAddress, seedPhrase, web
frame: { frame: {
version: "1", version: "1",
name: process.env.NEXT_PUBLIC_FRAME_NAME, name: process.env.NEXT_PUBLIC_FRAME_NAME,
iconUrl: process.env.NEXT_PUBLIC_FRAME_ICON_IMAGE_URL || `https://${domain}/icon.png`, iconUrl: `https://${trimmedDomain}/icon.png`,
homeUrl: domain, homeUrl: trimmedDomain,
imageUrl: `https://${domain}/opengraph-image`, imageUrl: `https://${trimmedDomain}/opengraph-image`,
buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT, buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT,
splashImageUrl: process.env.NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL || `https://${domain}/splash.png`, splashImageUrl: `https://${trimmedDomain}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",
webhookUrl, webhookUrl,
}, },
@ -78,16 +79,26 @@ async function loadEnvLocal() {
console.log('Loading values from .env.local...'); console.log('Loading values from .env.local...');
const localEnv = dotenv.parse(fs.readFileSync('.env.local')); const localEnv = dotenv.parse(fs.readFileSync('.env.local'));
// Copy all values except SEED_PHRASE to .env // Define allowed variables to load from .env.local
const allowedVars = [
'SEED_PHRASE',
'NEXT_PUBLIC_FRAME_NAME',
'NEXT_PUBLIC_FRAME_DESCRIPTION',
'NEXT_PUBLIC_FRAME_BUTTON_TEXT',
'NEYNAR_API_KEY',
'NEYNAR_CLIENT_ID'
];
// Copy allowed values except SEED_PHRASE to .env
const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : ''; const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : '';
let newEnvContent = envContent; let newEnvContent = envContent;
for (const [key, value] of Object.entries(localEnv)) { for (const [key, value] of Object.entries(localEnv)) {
if (key !== 'SEED_PHRASE') { if (allowedVars.includes(key)) {
// Update process.env // Update process.env
process.env[key] = value; process.env[key] = value;
// Add to .env content if not already there // Add to .env content if not already there (except for SEED_PHRASE)
if (!envContent.includes(`${key}=`)) { if (key !== 'SEED_PHRASE' && !envContent.includes(`${key}=`)) {
newEnvContent += `${key}="${value}"\n`; newEnvContent += `${key}="${value}"\n`;
} }
} }
@ -98,14 +109,6 @@ async function loadEnvLocal() {
console.log('✅ Values from .env.local have been written to .env'); console.log('✅ Values from .env.local have been written to .env');
} }
} }
// Always try to load SEED_PHRASE from .env.local
if (fs.existsSync('.env.local')) {
const localEnv = dotenv.parse(fs.readFileSync('.env.local'));
if (localEnv.SEED_PHRASE) {
process.env.SEED_PHRASE = localEnv.SEED_PHRASE;
}
}
} catch (error) { } catch (error) {
// Error reading .env.local, which is fine // Error reading .env.local, which is fine
console.log('Note: No .env.local file found'); console.log('Note: No .env.local file found');
@ -156,8 +159,9 @@ async function checkRequiredEnvVars() {
// Check if the variable already exists in .env // Check if the variable already exists in .env
if (!envContent.includes(`${varConfig.name}=`)) { if (!envContent.includes(`${varConfig.name}=`)) {
// Append the new variable to .env // Append the new variable to .env without extra newlines
fs.appendFileSync('.env', `\n${varConfig.name}="${value}"`); const newLine = envContent ? '\n' : '';
fs.appendFileSync('.env', `${newLine}${varConfig.name}="${value.trim()}"`);
} }
} }
} }
@ -294,52 +298,50 @@ async function deployToVercel(useGitHub = false) {
}, null, 2)); }, null, 2));
} }
// First try to link to an existing project // TODO: check if project already exists here
console.log('\n🔗 Checking for existing Vercel projects...');
let isNewProject = false;
let projectName = '';
let domain = '';
try { // Set up Vercel project
execSync('vercel link', { console.log('\n📦 Setting up Vercel project...');
cwd: projectRoot, console.log(' An initial deployment is required to get an assigned domain that can be used in the frame manifest\n');
stdio: 'inherit' console.log('\n⚠ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n');
}); execSync('vercel', {
cwd: projectRoot,
stdio: 'inherit'
});
// Get project info after linking // Load project info from .vercel/project.json
// question: do these lines do anything? const projectJson = JSON.parse(fs.readFileSync('.vercel/project.json', 'utf8'));
const projectOutput = execSync('vercel project ls', { const projectId = projectJson.projectId;
cwd: projectRoot,
encoding: 'utf8'
});
// Extract domain from project output // Get project details using project inspect
const projectLines = projectOutput.split('\n'); console.log('\n🔍 Getting project details...');
const currentProject = projectLines.find(line => line.includes('(current)')); const inspectOutput = execSync(`vercel project inspect ${projectId} 2>&1`, {
if (currentProject) { cwd: projectRoot,
const parts = currentProject.split(/\s+/); encoding: 'utf8'
projectName = parts[0]; });
domain = parts[1]?.replace('https://', '') || ''; console.log('inspectOutput');
console.log('🌐 Found existing project domain:', domain); console.log(inspectOutput);
} else {
throw new Error('No existing project found');
}
} catch (error) {
// If linking fails (user declines to link), create a new project
console.log('\n📦 Creating new Vercel project...');
execSync('vercel', {
cwd: projectRoot,
stdio: 'inherit'
});
// Use NEXT_PUBLIC_FRAME_NAME for domain, replacing spaces with dashes // Extract project name from inspect output
projectName = process.env.NEXT_PUBLIC_FRAME_NAME.toLowerCase().replace(/\s+/g, '-'); let projectName;
let domain;
const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/);
if (nameMatch) {
projectName = nameMatch[1].trim();
domain = `${projectName}.vercel.app`; domain = `${projectName}.vercel.app`;
isNewProject = true; console.log('🌐 Using project name for domain:', domain);
} else {
// Try alternative format
const altMatch = inspectOutput.match(/Found Project [^/]+\/([^\n]+)/);
if (altMatch) {
projectName = altMatch[1].trim();
domain = `${projectName}.vercel.app`;
console.log('🌐 Using project name for domain:', domain);
} else {
throw new Error('Could not determine project name from inspection output');
}
} }
console.log('🌐 Using frame name for domain:', domain);
// Generate frame metadata if we have a seed phrase // Generate frame metadata if we have a seed phrase
let frameMetadata; let frameMetadata;
if (process.env.SEED_PHRASE) { if (process.env.SEED_PHRASE) {
@ -362,6 +364,7 @@ async function deployToVercel(useGitHub = false) {
NEXTAUTH_SECRET: nextAuthSecret, NEXTAUTH_SECRET: nextAuthSecret,
AUTH_SECRET: nextAuthSecret, // Fallback for some NextAuth versions AUTH_SECRET: nextAuthSecret, // Fallback for some NextAuth versions
NEXTAUTH_URL: `https://${domain}`, // Add the deployment URL NEXTAUTH_URL: `https://${domain}`, // Add the deployment URL
NEXT_PUBLIC_URL: `https://${domain}`,
// Optional vars that should be set if they exist // Optional vars that should be set if they exist
...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }), ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }),
@ -428,75 +431,75 @@ async function deployToVercel(useGitHub = false) {
}); });
} }
// For new projects, verify the actual domain after deployment // Verify the actual domain after deployment
if (isNewProject) {
console.log('\n🔍 Verifying deployment domain...');
const projectOutput = execSync('vercel project ls', {
cwd: projectRoot,
encoding: 'utf8'
});
const projectLines = projectOutput.split('\n'); console.log('\n🔍 Verifying deployment domain...');
const currentProject = projectLines.find(line => line.includes('(current)')); const projectOutput = execSync('vercel project ls', {
if (currentProject) { cwd: projectRoot,
const actualDomain = currentProject.split(/\s+/)[1]?.replace('https://', ''); encoding: 'utf8'
if (actualDomain && actualDomain !== domain) { });
console.log(`⚠️ Actual domain (${actualDomain}) differs from assumed domain (${domain})`);
console.log('🔄 Updating environment variables with correct domain...');
// Update domain-dependent environment variables const projectLines = projectOutput.split('\n');
const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID const currentProject = projectLines.find(line => line.includes('(current)'));
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` if (currentProject) {
: `https://${actualDomain}/api/webhook`; const actualDomain = currentProject.split(/\s+/)[1]?.replace('https://', '');
if (actualDomain && actualDomain !== domain) {
console.log(`⚠️ Actual domain (${actualDomain}) differs from assumed domain (${domain})`);
console.log('🔄 Updating environment variables with correct domain...');
if (frameMetadata) { // Update domain-dependent environment variables
frameMetadata = await generateFarcasterMetadata(actualDomain, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl); const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
// Update FRAME_METADATA env var ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
try { : `https://${actualDomain}/api/webhook`;
execSync(`vercel env rm FRAME_METADATA production -y`, {
cwd: projectRoot,
stdio: 'ignore',
env: process.env
});
execSync(`echo "${JSON.stringify(frameMetadata)}" | vercel env add FRAME_METADATA production`, {
cwd: projectRoot,
stdio: 'inherit',
env: process.env
});
} catch (error) {
console.warn('⚠️ Warning: Failed to update FRAME_METADATA with correct domain');
}
}
// Update NEXTAUTH_URL if (frameMetadata) {
frameMetadata = await generateFarcasterMetadata(actualDomain, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl);
// Update FRAME_METADATA env var
try { try {
execSync(`vercel env rm NEXTAUTH_URL production -y`, { execSync(`vercel env rm FRAME_METADATA production -y`, {
cwd: projectRoot, cwd: projectRoot,
stdio: 'ignore', stdio: 'ignore',
env: process.env env: process.env
}); });
execSync(`echo "https://${actualDomain}" | vercel env add NEXTAUTH_URL production`, { execSync(`echo "${JSON.stringify(frameMetadata)}" | vercel env add FRAME_METADATA production`, {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit', stdio: 'inherit',
env: process.env env: process.env
}); });
} catch (error) { } catch (error) {
console.warn('⚠️ Warning: Failed to update NEXTAUTH_URL with correct domain'); console.warn('⚠️ Warning: Failed to update FRAME_METADATA with correct domain');
} }
}
// Redeploy with updated environment variables // Update NEXTAUTH_URL
console.log('\n📦 Redeploying with correct domain...'); try {
execSync('vercel deploy --prod', { execSync(`vercel env rm NEXTAUTH_URL production -y`, {
cwd: projectRoot,
stdio: 'ignore',
env: process.env
});
execSync(`echo "https://${actualDomain}" | vercel env add NEXTAUTH_URL production`, {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit', stdio: 'inherit',
env: process.env env: process.env
}); });
} catch (error) {
domain = actualDomain; console.warn('⚠️ Warning: Failed to update NEXTAUTH_URL with correct domain');
} }
// Redeploy with updated environment variables
console.log('\n📦 Redeploying with correct domain...');
execSync('vercel deploy --prod', {
cwd: projectRoot,
stdio: 'inherit',
env: process.env
});
domain = actualDomain;
} }
} }
console.log('\n✨ Deployment complete! Your frame is now live at:'); console.log('\n✨ Deployment complete! Your frame is now live at:');
console.log(`🌐 https://${domain}`); console.log(`🌐 https://${domain}`);
console.log('\n📝 You can manage your project at https://vercel.com/dashboard'); console.log('\n📝 You can manage your project at https://vercel.com/dashboard');