diff --git a/bin/init.js b/bin/init.js index 167969a..0ab7101 100644 --- a/bin/init.js +++ b/bin/init.js @@ -425,7 +425,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) { "pino-pretty": "^13.0.0", "postcss": "^8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "ts-node": "^10.9.2" }; // Add Neynar SDK if selected diff --git a/package.json b/package.json index b5a2f78..e83cc8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.6", + "version": "1.6.0", "type": "module", "private": false, "access": "public", @@ -31,11 +31,11 @@ ], "scripts": { "dev": "node scripts/dev.js", - "build": "node scripts/build.js", + "build": "next build", "build:raw": "next build", "start": "next start", "lint": "next lint", - "deploy:vercel": "node scripts/deploy.js", + "deploy:vercel": "ts-node scripts/deploy.ts", "deploy:raw": "vercel --prod", "cleanup": "node scripts/cleanup.js" }, diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100755 index b680399..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,425 +0,0 @@ -import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { mnemonicToAccount } from 'viem/accounts'; -import { fileURLToPath } from 'url'; -import inquirer from 'inquirer'; -import dotenv from 'dotenv'; -import crypto from 'crypto'; - -// ANSI color codes -const yellow = '\x1b[33m'; -const italic = '\x1b[3m'; -const reset = '\x1b[0m'; - -// Load environment variables in specific order -// First load .env for main config -dotenv.config({ path: '.env' }); - -async function lookupFidByCustodyAddress(custodyAddress, apiKey) { - if (!apiKey) { - throw new Error('Neynar API key is required'); - } - const lowerCasedCustodyAddress = custodyAddress.toLowerCase(); - - const response = await fetch( - `https://api.neynar.com/v2/farcaster/user/bulk-by-address?addresses=${lowerCasedCustodyAddress}&address_types=custody_address`, - { - headers: { - 'accept': 'application/json', - 'x-api-key': 'FARCASTER_V2_FRAMES_DEMO' - } - } - ); - - if (!response.ok) { - throw new Error(`Failed to lookup FID: ${response.statusText}`); - } - - const data = await response.json(); - if (!data[lowerCasedCustodyAddress]?.length || !data[lowerCasedCustodyAddress][0].custody_address) { - throw new Error('No FID found for this custody address'); - } - - return data[lowerCasedCustodyAddress][0].fid; -} - -async function loadEnvLocal() { - try { - if (fs.existsSync('.env.local')) { - const { loadLocal } = await inquirer.prompt([ - { - type: 'confirm', - name: 'loadLocal', - message: 'Found .env.local, likely created by the install script - would you like to load its values?', - default: false - } - ]); - - if (loadLocal) { - console.log('Loading values from .env.local...'); - const localEnv = dotenv.parse(fs.readFileSync('.env.local')); - - // Copy all values except SEED_PHRASE to .env - const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : ''; - let newEnvContent = envContent; - - for (const [key, value] of Object.entries(localEnv)) { - if (key !== 'SEED_PHRASE') { - // Update process.env - process.env[key] = value; - // Add to .env content if not already there - if (!envContent.includes(`${key}=`)) { - newEnvContent += `${key}="${value}"\n`; - } - } - } - - // Write updated content to .env - fs.writeFileSync('.env', newEnvContent); - 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) { - // Error reading .env.local, which is fine - console.log('Note: No .env.local file found'); - } -} - -// TODO: make sure rebuilding is supported - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.join(__dirname, '..'); - -async function validateDomain(domain) { - // Remove http:// or https:// if present - const cleanDomain = domain.replace(/^https?:\/\//, ''); - - // Basic domain validation - if (!cleanDomain.match(/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/)) { - throw new Error('Invalid domain format'); - } - - return cleanDomain; -} - -async function queryNeynarApp(apiKey) { - if (!apiKey) { - return null; - } - try { - const response = await fetch( - `https://api.neynar.com/portal/app_by_api_key`, - { - headers: { - 'x-api-key': apiKey - } - } - ); - const data = await response.json(); - return data; - } catch (error) { - console.error('Error querying Neynar app data:', error); - return null; - } -} - -async function validateSeedPhrase(seedPhrase) { - try { - // Try to create an account from the seed phrase - const account = mnemonicToAccount(seedPhrase); - return account.address; - } catch (error) { - throw new Error('Invalid seed phrase'); - } -} - -async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase, webhookUrl) { - const header = { - type: 'custody', - key: accountAddress, - fid, - }; - const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); - - const payload = { - domain - }; - const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); - - const account = mnemonicToAccount(seedPhrase); - const signature = await account.signMessage({ - message: `${encodedHeader}.${encodedPayload}` - }); - const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); - - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); - - return { - accountAssociation: { - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature - }, - frame: { - version: "1", - name: process.env.NEXT_PUBLIC_MINI_APP_NAME, - iconUrl: `https://${domain}/icon.png`, - homeUrl: `https://${domain}`, - imageUrl: `https://${domain}/api/opengraph-image`, - buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT, - splashImageUrl: `https://${domain}/splash.png`, - splashBackgroundColor: "#f7f7f7", - webhookUrl, - description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, - primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, - tags, - }, - }; -} - -async function main() { - try { - console.log('\nšŸ“ Checking environment variables...'); - console.log('Loading values from .env...'); - - // Load .env.local if user wants to - await loadEnvLocal(); - - // Get domain from user - const { domain } = await inquirer.prompt([ - { - type: 'input', - name: 'domain', - message: 'Enter the domain where your mini app will be deployed (e.g., example.com):', - validate: async (input) => { - try { - await validateDomain(input); - return true; - } catch (error) { - return error.message; - } - } - } - ]); - - // Get frame name from user - const { frameName } = await inquirer.prompt([ - { - type: 'input', - name: 'frameName', - message: 'Enter the name for your mini app (e.g., My Cool Mini App):', - default: process.env.NEXT_PUBLIC_MINI_APP_NAME, - validate: (input) => { - if (input.trim() === '') { - return 'Mini app name cannot be empty'; - } - return true; - } - } - ]); - - // Get button text from user - const { buttonText } = await inquirer.prompt([ - { - type: 'input', - name: 'buttonText', - message: 'Enter the text for your mini app button:', - default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || 'Launch Mini App', - validate: (input) => { - if (input.trim() === '') { - return 'Button text cannot be empty'; - } - return true; - } - } - ]); - - // Get Neynar configuration - let neynarApiKey = process.env.NEYNAR_API_KEY; - let neynarClientId = process.env.NEYNAR_CLIENT_ID; - let useNeynar = true; - - while (useNeynar) { - if (!neynarApiKey) { - const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([ - { - type: 'password', - name: 'neynarApiKey', - message: 'Enter your Neynar API key (optional - leave blank to skip):', - default: null - } - ]); - neynarApiKey = inputNeynarApiKey; - } else { - console.log('Using existing Neynar API key from .env'); - } - - if (!neynarApiKey) { - useNeynar = false; - break; - } - - // Try to get client ID from API - if (!neynarClientId) { - const appInfo = await queryNeynarApp(neynarApiKey); - if (appInfo) { - neynarClientId = appInfo.app_uuid; - console.log('āœ… Fetched Neynar app client ID'); - break; - } - } - - // We have a client ID (either from .env or fetched from API), so we can break out of the loop - if (neynarClientId) { - break; - } - - // If we get here, the API key was invalid - console.log('\nāš ļø Could not find Neynar app information. The API key may be incorrect.'); - const { retry } = await inquirer.prompt([ - { - type: 'confirm', - name: 'retry', - message: 'Would you like to try a different API key?', - default: true - } - ]); - - // Reset for retry - neynarApiKey = null; - neynarClientId = null; - - if (!retry) { - useNeynar = false; - break; - } - } - - // Get seed phrase from user - let seedPhrase = process.env.SEED_PHRASE; - if (!seedPhrase) { - const { seedPhrase: inputSeedPhrase } = await inquirer.prompt([ - { - type: 'password', - name: 'seedPhrase', - message: 'Your farcaster custody account seed phrase is required to create a signature proving this app was created by you.\n' + - `āš ļø ${yellow}${italic}seed phrase is only used to sign the mini app manifest, then discarded${reset} āš ļø\n` + - 'Seed phrase:', - validate: async (input) => { - try { - await validateSeedPhrase(input); - return true; - } catch (error) { - return error.message; - } - } - } - ]); - seedPhrase = inputSeedPhrase; - } else { - console.log('Using existing seed phrase from .env'); - } - - // Validate seed phrase and get account address - const accountAddress = await validateSeedPhrase(seedPhrase); - console.log('āœ… Generated account address from seed phrase'); - - const fid = await lookupFidByCustodyAddress(accountAddress, neynarApiKey ?? 'FARCASTER_V2_FRAMES_DEMO'); - - // Generate and sign manifest - console.log('\nšŸ”Ø Generating mini app manifest...'); - - // Determine webhook URL based on environment variables - const webhookUrl = neynarApiKey && neynarClientId - ? `https://api.neynar.com/f/app/${neynarClientId}/event` - : `${domain}/api/webhook`; - - const metadata = await generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase, webhookUrl); - console.log('\nāœ… Mini app manifest generated' + (seedPhrase ? ' and signed' : '')); - - // Read existing .env file or create new one - const envPath = path.join(projectRoot, '.env'); - let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''; - - // Add or update environment variables - const newEnvVars = [ - // Base URL - `NEXT_PUBLIC_URL=https://${domain}`, - - // Mini app metadata - `NEXT_PUBLIC_MINI_APP_NAME="${frameName}"`, - `NEXT_PUBLIC_MINI_APP_DESCRIPTION="${process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || ''}"`, - `NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || ''}"`, - `NEXT_PUBLIC_MINI_APP_TAGS="${process.env.NEXT_PUBLIC_MINI_APP_TAGS || ''}"`, - `NEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${buttonText}"`, - - // Analytics - `NEXT_PUBLIC_ANALYTICS_ENABLED="${process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false'}"`, - - // Neynar configuration (if it exists in current env) - ...(process.env.NEYNAR_API_KEY ? - [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), - ...(neynarClientId ? - [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []), - - // FID (if it exists in current env) - ...(process.env.FID ? [`FID="${process.env.FID}"`] : []), - `NEXT_PUBLIC_USE_WALLET="${process.env.NEXT_PUBLIC_USE_WALLET || 'false'}"`, - - // NextAuth configuration - `NEXTAUTH_SECRET="${process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex')}"`, - `NEXTAUTH_URL="https://${domain}"`, - - // Mini app manifest with signature - `MINI_APP_METADATA=${JSON.stringify(metadata)}`, - ]; - - // Filter out empty values and join with newlines - const validEnvVars = newEnvVars.filter(line => { - const [, value] = line.split('='); - return value && value !== '""'; - }); - - // Update or append each environment variable - validEnvVars.forEach(varLine => { - const [key] = varLine.split('='); - if (envContent.includes(`${key}=`)) { - envContent = envContent.replace(new RegExp(`${key}=.*`), varLine); - } else { - envContent += `\n${varLine}`; - } - }); - - // Write updated .env file - fs.writeFileSync(envPath, envContent); - - console.log('\nāœ… Environment variables updated'); - - // Run next build - console.log('\nBuilding Next.js application...'); - const nextBin = path.normalize(path.join(projectRoot, 'node_modules', '.bin', 'next')); - execSync(`"${nextBin}" build`, { - cwd: projectRoot, - stdio: 'inherit', - shell: process.platform === 'win32' - }); - - console.log('\n✨ Build complete! Your mini app is ready for deployment. 🪐'); - console.log('šŸ“ Make sure to configure the environment variables from .env in your hosting provider'); - - } catch (error) { - console.error('\nāŒ Error:', error.message); - process.exit(1); - } -} - -main(); diff --git a/scripts/deploy.js b/scripts/deploy.ts similarity index 67% rename from scripts/deploy.js rename to scripts/deploy.ts index 80c8db2..0578e91 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.ts @@ -6,8 +6,10 @@ import { fileURLToPath } from 'url'; import inquirer from 'inquirer'; import dotenv from 'dotenv'; import crypto from 'crypto'; -import { mnemonicToAccount } from 'viem/accounts'; -import { Vercel } from '@vercel/sdk'; +// Add fallback type for Vercel if type is missing +// @ts-ignore +import { Vercel as VercelSDKType } from '@vercel/sdk'; +import { APP_NAME, APP_BUTTON_TEXT } from '../src/lib/constants'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, '..'); @@ -15,92 +17,10 @@ const projectRoot = path.join(__dirname, '..'); // Load environment variables in specific order dotenv.config({ path: '.env' }); -async function validateSeedPhrase(seedPhrase) { - try { - const account = mnemonicToAccount(seedPhrase); - return account.address; - } catch (error) { - throw new Error('Invalid seed phrase'); - } -} - -async function lookupFidByCustodyAddress(custodyAddress, apiKey) { - if (!apiKey) { - throw new Error('Neynar API key is required'); - } - const lowerCasedCustodyAddress = custodyAddress.toLowerCase(); - - const response = await fetch( - `https://api.neynar.com/v2/farcaster/user/bulk-by-address?addresses=${lowerCasedCustodyAddress}&address_types=custody_address`, - { - headers: { - 'accept': 'application/json', - 'x-api-key': apiKey - } - } - ); - - if (!response.ok) { - throw new Error(`Failed to lookup FID: ${response.statusText}`); - } - - const data = await response.json(); - if (!data[lowerCasedCustodyAddress]?.length || !data[lowerCasedCustodyAddress][0].custody_address) { - throw new Error('No FID found for this custody address'); - } - - return data[lowerCasedCustodyAddress][0].fid; -} - -async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase, webhookUrl) { - const trimmedDomain = domain.trim(); - const header = { - type: 'custody', - key: accountAddress, - fid, - }; - const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); - - const payload = { - domain: trimmedDomain - }; - const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); - - const account = mnemonicToAccount(seedPhrase); - const signature = await account.signMessage({ - message: `${encodedHeader}.${encodedPayload}` - }); - const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); - - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); - - return { - accountAssociation: { - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature - }, - frame: { - version: "1", - name: process.env.NEXT_PUBLIC_MINI_APP_NAME, - iconUrl: `https://${trimmedDomain}/icon.png`, - homeUrl: `https://${trimmedDomain}`, - imageUrl: `https://${trimmedDomain}/api/opengraph-image`, - buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT, - splashImageUrl: `https://${trimmedDomain}/splash.png`, - splashBackgroundColor: "#f7f7f7", - webhookUrl: webhookUrl?.trim(), - description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, - primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, - tags, - }, - }; -} - -async function loadEnvLocal() { +async function loadEnvLocal(): Promise { try { if (fs.existsSync('.env.local')) { - const { loadLocal } = await inquirer.prompt([ + const { loadLocal }: { loadLocal: boolean } = await inquirer.prompt([ { type: 'confirm', name: 'loadLocal', @@ -115,12 +35,6 @@ async function loadEnvLocal() { const allowedVars = [ 'SEED_PHRASE', - 'NEXT_PUBLIC_MINI_APP_NAME', - 'NEXT_PUBLIC_MINI_APP_DESCRIPTION', - 'NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY', - 'NEXT_PUBLIC_MINI_APP_TAGS', - 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', - 'NEXT_PUBLIC_ANALYTICS_ENABLED', 'NEYNAR_API_KEY', 'NEYNAR_CLIENT_ID' ]; @@ -141,12 +55,12 @@ async function loadEnvLocal() { console.log('āœ… Values from .env.local have been written to .env'); } } - } catch (error) { + } catch (error: unknown) { console.log('Note: No .env.local file found'); } } -async function checkRequiredEnvVars() { +async function checkRequiredEnvVars(): Promise { console.log('\nšŸ“ Checking environment variables...'); console.log('Loading values from .env...'); @@ -156,14 +70,14 @@ async function checkRequiredEnvVars() { { name: 'NEXT_PUBLIC_MINI_APP_NAME', message: 'Enter the name for your frame (e.g., My Cool Mini App):', - default: process.env.NEXT_PUBLIC_MINI_APP_NAME, - validate: input => input.trim() !== '' || 'Mini app name cannot be empty' + default: APP_NAME, + validate: (input: string) => input.trim() !== '' || 'Mini app name cannot be empty' }, { name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', message: 'Enter the text for your frame button:', - default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App', - validate: input => input.trim() !== '' || 'Button text cannot be empty' + default: APP_BUTTON_TEXT ?? 'Launch Mini App', + validate: (input: string) => input.trim() !== '' || 'Button text cannot be empty' } ]; @@ -192,75 +106,45 @@ async function checkRequiredEnvVars() { } } } - - // Check for seed phrase - if (!process.env.SEED_PHRASE) { - console.log('\nšŸ”‘ Mini App Manifest Signing'); - console.log('A signed manifest helps users trust your mini app.'); - const { seedPhrase } = await inquirer.prompt([ - { - type: 'password', - name: 'seedPhrase', - message: 'Enter your Farcaster custody account seed phrase to sign the mini app manifest\n(optional -- leave blank to create an unsigned mini app)\n\nSeed phrase:', - default: null - } - ]); - - if (seedPhrase) { - process.env.SEED_PHRASE = seedPhrase; - - const { storeSeedPhrase } = await inquirer.prompt([ - { - type: 'confirm', - name: 'storeSeedPhrase', - message: 'Would you like to store this seed phrase in .env.local for future use?', - default: false - } - ]); - - if (storeSeedPhrase) { - fs.appendFileSync('.env.local', `\nSEED_PHRASE="${seedPhrase}"`); - console.log('āœ… Seed phrase stored in .env.local'); - } else { - console.log('ā„¹ļø Seed phrase will only be used for this deployment'); - } - } - } } -async function getGitRemote() { +async function getGitRemote(): Promise { try { const remoteUrl = execSync('git remote get-url origin', { cwd: projectRoot, encoding: 'utf8' }).trim(); return remoteUrl; - } catch (error) { - return null; + } catch (error: unknown) { + if (error instanceof Error) { + return null; + } + throw error; } } -async function checkVercelCLI() { +async function checkVercelCLI(): Promise { try { execSync('vercel --version', { - stdio: 'ignore', - shell: process.platform === 'win32' + stdio: 'ignore' }); return true; - } catch (error) { - return false; + } catch (error: unknown) { + if (error instanceof Error) { + return false; + } + throw error; } } -async function installVercelCLI() { +async function installVercelCLI(): Promise { console.log('Installing Vercel CLI...'); execSync('npm install -g vercel', { - stdio: 'inherit', - shell: process.platform === 'win32' + stdio: 'inherit' }); } -async function getVercelToken() { +async function getVercelToken(): Promise { try { // Try to get token from Vercel CLI config const configPath = path.join(os.homedir(), '.vercel', 'auth.json'); @@ -268,8 +152,10 @@ async function getVercelToken() { const authConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); return authConfig.token; } - } catch (error) { - console.warn('Could not read Vercel token from config file'); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('Could not read Vercel token from config file'); + } } // Try environment variable @@ -288,12 +174,15 @@ async function getVercelToken() { // The token isn't directly exposed, so we'll need to use CLI for some operations console.log('āœ… Verified Vercel CLI authentication'); return null; // We'll fall back to CLI operations - } catch (error) { - throw new Error('Not logged in to Vercel CLI. Please run this script again to login.'); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error('Not logged in to Vercel CLI. Please run this script again to login.'); + } + throw error; } } -async function loginToVercel() { +async function loginToVercel(): Promise { console.log('\nšŸ”‘ Vercel Login'); console.log('You can either:'); console.log('1. Log in to an existing Vercel account'); @@ -309,7 +198,7 @@ async function loginToVercel() { stdio: 'inherit' }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { child.on('close', (code) => { resolve(); }); @@ -323,8 +212,8 @@ async function loginToVercel() { execSync('vercel whoami', { stdio: 'ignore' }); console.log('āœ… Successfully logged in to Vercel!'); return true; - } catch (error) { - if (error.message.includes('Account not found')) { + } catch (error: unknown) { + if (error instanceof Error && error.message.includes('Account not found')) { console.log('ā„¹ļø Waiting for Vercel account setup to complete...'); } await new Promise(resolve => setTimeout(resolve, 2000)); @@ -338,9 +227,9 @@ async function loginToVercel() { return false; } -async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { +async function setVercelEnvVarSDK(vercelClient: VercelSDKType, projectId: string, key: string, value: string | object): Promise { try { - let processedValue; + let processedValue: string; if (typeof value === 'object') { processedValue = JSON.stringify(value); } else { @@ -352,7 +241,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { idOrName: projectId }); - const existingVar = existingVars.envs?.find(env => + const existingVar = existingVars.envs?.find((env: any) => env.key === key && env.target?.includes('production') ); @@ -382,13 +271,16 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { } return true; - } catch (error) { - console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); - return false; + } catch (error: unknown) { + if (error instanceof Error) { + console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); + return false; + } + throw error; } } -async function setVercelEnvVarCLI(key, value, projectRoot) { +async function setVercelEnvVarCLI(key: string, value: string | object, projectRoot: string): Promise { try { // Remove existing env var try { @@ -397,11 +289,11 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { stdio: 'ignore', env: process.env }); - } catch (error) { + } catch (error: unknown) { // Ignore errors from removal } - let processedValue; + let processedValue: string; if (typeof value === 'object') { processedValue = JSON.stringify(value); } else { @@ -413,7 +305,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { fs.writeFileSync(tempFilePath, processedValue, 'utf8'); // Use appropriate command based on platform - let command; + let command: string; if (process.platform === 'win32') { command = `type "${tempFilePath}" | vercel env add ${key} production`; } else { @@ -423,27 +315,29 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { execSync(command, { cwd: projectRoot, stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts - shell: true, env: process.env }); fs.unlinkSync(tempFilePath); console.log(`āœ… Set environment variable: ${key}`); return true; - } catch (error) { + } catch (error: unknown) { const tempFilePath = path.join(projectRoot, `${key}_temp.txt`); if (fs.existsSync(tempFilePath)) { fs.unlinkSync(tempFilePath); } - console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); - return false; + if (error instanceof Error) { + console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); + return false; + } + throw error; } } -async function setEnvironmentVariables(vercelClient, projectId, envVars, projectRoot) { +async function setEnvironmentVariables(vercelClient: VercelSDKType | null, projectId: string | null, envVars: Record, projectRoot: string): Promise> { console.log('\nšŸ“ Setting up environment variables...'); - const results = []; + const results: Array<{ key: string; success: boolean }> = []; for (const [key, value] of Object.entries(envVars)) { if (!value) continue; @@ -474,18 +368,18 @@ async function setEnvironmentVariables(vercelClient, projectId, envVars, project return results; } -async function waitForDeployment(vercelClient, projectId, maxWaitTime = 300000) { // 5 minutes +async function waitForDeployment(vercelClient: VercelSDKType | null, projectId: string, maxWaitTime = 300000): Promise { // 5 minutes console.log('\nā³ Waiting for deployment to complete...'); const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { try { - const deployments = await vercelClient.deployments.list({ + const deployments = await vercelClient?.deployments.list({ projectId: projectId, limit: 1 }); - if (deployments.deployments?.[0]) { + if (deployments?.deployments?.[0]) { const deployment = deployments.deployments[0]; console.log(`šŸ“Š Deployment status: ${deployment.state}`); @@ -504,16 +398,19 @@ async function waitForDeployment(vercelClient, projectId, maxWaitTime = 300000) console.log('ā³ No deployment found yet, waiting...'); await new Promise(resolve => setTimeout(resolve, 5000)); } - } catch (error) { - console.warn('āš ļø Could not check deployment status:', error.message); - await new Promise(resolve => setTimeout(resolve, 5000)); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not check deployment status:', error.message); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + throw error; } } throw new Error('Deployment timed out after 5 minutes'); } -async function deployToVercel(useGitHub = false) { +async function deployToVercel(useGitHub = false): Promise { try { console.log('\nšŸš€ Deploying to Vercel...'); @@ -535,18 +432,18 @@ async function deployToVercel(useGitHub = false) { // Use spawn instead of execSync for better error handling const { spawn } = await import('child_process'); const vercelSetup = spawn('vercel', [], { - cwd: projectRoot, - stdio: 'inherit', - shell: process.platform === 'win32' - }); + cwd: projectRoot, + stdio: 'inherit', + shell: process.platform === 'win32' ? true : undefined + }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { vercelSetup.on('close', (code) => { if (code === 0 || code === null) { console.log('āœ… Vercel project setup completed'); resolve(); } else { - console.log('āš ļø Vercel setup command completed (this is normal)'); + console.log('āš ļø Vercel setup command completed (this is normal)'); resolve(); // Don't reject, as this is often expected } }); @@ -561,32 +458,38 @@ async function deployToVercel(useGitHub = false) { await new Promise(resolve => setTimeout(resolve, 2000)); // Load project info - let projectId; + let projectId: string; try { const projectJson = JSON.parse(fs.readFileSync('.vercel/project.json', 'utf8')); projectId = projectJson.projectId; - } catch (error) { - throw new Error('Failed to load project info. Please ensure the Vercel project was created successfully.'); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error('Failed to load project info. Please ensure the Vercel project was created successfully.'); + } + throw error; } // Get Vercel token and initialize SDK client - let vercelClient = null; + let vercelClient: VercelSDKType | null = null; try { const token = await getVercelToken(); if (token) { - vercelClient = new Vercel({ + vercelClient = new VercelSDKType({ bearerToken: token }); console.log('āœ… Initialized Vercel SDK client'); } - } catch (error) { - console.warn('āš ļø Could not initialize Vercel SDK, falling back to CLI operations'); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not initialize Vercel SDK, falling back to CLI operations'); + } + throw error; } // Get project details console.log('\nšŸ” Getting project details...'); - let domain; - let projectName; + let domain: string | undefined; + let projectName: string | undefined; if (vercelClient) { try { @@ -596,8 +499,11 @@ async function deployToVercel(useGitHub = false) { projectName = project.name; domain = `${projectName}.vercel.app`; console.log('🌐 Using project name for domain:', domain); - } catch (error) { - console.warn('āš ļø Could not get project details via SDK, using CLI fallback'); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not get project details via SDK, using CLI fallback'); + } + throw error; } } @@ -627,30 +533,17 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Using fallback domain:', domain); } } - } catch (error) { - console.warn('āš ļø Could not inspect project, using fallback domain'); - // Use a fallback domain based on project ID - domain = `project-${projectId.slice(-8)}.vercel.app`; - console.log('🌐 Using fallback domain:', domain); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not inspect project, using fallback domain'); + // Use a fallback domain based on project ID + domain = `project-${projectId.slice(-8)}.vercel.app`; + console.log('🌐 Using fallback domain:', domain); + } + throw error; } } - // Generate mini app metadata if we have a seed phrase - let miniAppMetadata; - let fid; - if (process.env.SEED_PHRASE) { - console.log('\nšŸ”Ø Generating mini app metadata...'); - const accountAddress = await validateSeedPhrase(process.env.SEED_PHRASE); - fid = await lookupFidByCustodyAddress(accountAddress, process.env.NEYNAR_API_KEY ?? 'FARCASTER_V2_FRAMES_DEMO'); - - const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID - ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` - : `https://${domain}/api/webhook`; - - miniAppMetadata = await generateFarcasterMetadata(domain, fid, accountAddress, process.env.SEED_PHRASE, webhookUrl); - console.log('āœ… Mini app metadata generated and signed'); - } - // Prepare environment variables const nextAuthSecret = process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex'); const vercelEnv = { @@ -661,7 +554,6 @@ async function deployToVercel(useGitHub = false) { ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }), ...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }), - ...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }), ...Object.fromEntries( Object.entries(process.env) @@ -692,7 +584,7 @@ async function deployToVercel(useGitHub = false) { env: process.env }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { vercelDeploy.on('close', (code) => { if (code === 0) { console.log('āœ… Vercel deployment command completed'); @@ -710,13 +602,16 @@ async function deployToVercel(useGitHub = false) { }); // Wait for deployment to actually complete - let deployment; + let deployment: any; if (vercelClient) { try { deployment = await waitForDeployment(vercelClient, projectId); - } catch (error) { - console.warn('āš ļø Could not verify deployment completion:', error.message); - console.log('ā„¹ļø Proceeding with domain verification...'); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not verify deployment completion:', error.message); + console.log('ā„¹ļø Proceeding with domain verification...'); + } + throw error; } } @@ -727,30 +622,24 @@ async function deployToVercel(useGitHub = false) { if (vercelClient && deployment) { try { actualDomain = deployment.url || domain; - console.log('🌐 Verified actual domain:', actualDomain); - } catch (error) { - console.warn('āš ļø Could not verify domain via SDK, using assumed domain'); + console.log('🌐 Verified actual domain:', actualDomain); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not verify domain via SDK, using assumed domain'); + } + throw error; } } // Update environment variables if domain changed if (actualDomain !== domain) { console.log('šŸ”„ Updating environment variables with correct domain...'); - - const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID - ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` - : `https://${actualDomain}/api/webhook`; - const updatedEnv = { + const updatedEnv: Record = { NEXTAUTH_URL: `https://${actualDomain}`, NEXT_PUBLIC_URL: `https://${actualDomain}` }; - if (miniAppMetadata) { - const updatedMetadata = await generateFarcasterMetadata(actualDomain, fid, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl); - updatedEnv.MINI_APP_METADATA = updatedMetadata; - } - await setEnvironmentVariables(vercelClient, projectId, updatedEnv, projectRoot); console.log('\nšŸ“¦ Redeploying with correct domain...'); @@ -760,7 +649,7 @@ async function deployToVercel(useGitHub = false) { env: process.env }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { vercelRedeploy.on('close', (code) => { if (code === 0) { console.log('āœ… Redeployment completed'); @@ -784,13 +673,56 @@ async function deployToVercel(useGitHub = false) { console.log(`🌐 https://${domain}`); console.log('\nšŸ“ You can manage your project at https://vercel.com/dashboard'); - } catch (error) { - console.error('\nāŒ Deployment failed:', error.message); - process.exit(1); + // Prompt user to sign manifest in browser and paste accountAssociation + console.log(`\nāš ļø To complete your mini app manifest, you must sign it using the Farcaster developer portal.`); + console.log('1. Go to: https://farcaster.xyz/~/developers/mini-apps/manifest?domain=' + domain); + console.log('2. Click "Transfer Ownership" and follow the instructions to sign the manifest.'); + console.log('3. Copy the resulting accountAssociation JSON from the browser.'); + console.log('4. Paste it below when prompted.'); + + const { userAccountAssociation } = await inquirer.prompt([ + { + type: 'editor', + name: 'userAccountAssociation', + message: 'Paste the accountAssociation JSON here:', + validate: (input: string) => { + try { + const parsed = JSON.parse(input); + if (parsed.header && parsed.payload && parsed.signature) { + return true; + } + return 'Invalid accountAssociation: must have header, payload, and signature'; + } catch (e) { + return 'Invalid JSON'; + } + } + } + ]); + const parsedAccountAssociation = JSON.parse(userAccountAssociation); + + // Write APP_ACCOUNT_ASSOCIATION to src/lib/constants.ts + const constantsPath = path.join(projectRoot, 'src', 'lib', 'constants.ts'); + let constantsContent = fs.readFileSync(constantsPath, 'utf8'); + + // Replace the APP_ACCOUNT_ASSOCIATION line using a robust, anchored, multiline regex + const newAccountAssociation = `export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = ${JSON.stringify(parsedAccountAssociation, null, 2)};`; + constantsContent = constantsContent.replace( + /^export const APP_ACCOUNT_ASSOCIATION\s*:\s*AccountAssociation \| undefined\s*=\s*[^;]*;/m, + newAccountAssociation + ); + fs.writeFileSync(constantsPath, constantsContent); + console.log('\nāœ… APP_ACCOUNT_ASSOCIATION updated in src/lib/constants.ts'); + + } catch (error: unknown) { + if (error instanceof Error) { + console.error('\nāŒ Deployment failed:', error.message); + process.exit(1); + } + throw error; } } -async function main() { +async function main(): Promise { try { console.log('šŸš€ Vercel Mini App Deployment (SDK Edition)'); console.log('This script will deploy your mini app to Vercel using the Vercel SDK.'); @@ -803,13 +735,16 @@ async function main() { // Check if @vercel/sdk is installed try { await import('@vercel/sdk'); - } catch (error) { - console.log('šŸ“¦ Installing @vercel/sdk...'); - execSync('npm install @vercel/sdk', { - cwd: projectRoot, - stdio: 'inherit' - }); - console.log('āœ… @vercel/sdk installed successfully'); + } catch (error: unknown) { + if (error instanceof Error) { + console.log('šŸ“¦ Installing @vercel/sdk...'); + execSync('npm install @vercel/sdk', { + cwd: projectRoot, + stdio: 'inherit' + }); + console.log('āœ… @vercel/sdk installed successfully'); + } + throw error; } await checkRequiredEnvVars(); @@ -866,9 +801,12 @@ async function main() { await deployToVercel(useGitHub); - } catch (error) { - console.error('\nāŒ Error:', error.message); - process.exit(1); + } catch (error: unknown) { + if (error instanceof Error) { + console.error('\nāŒ Error:', error.message); + process.exit(1); + } + throw error; } } diff --git a/src/app/.well-known/farcaster.json/route.ts b/src/app/.well-known/farcaster.json/route.ts index 19ce6d7..d116c4f 100644 --- a/src/app/.well-known/farcaster.json/route.ts +++ b/src/app/.well-known/farcaster.json/route.ts @@ -1,9 +1,9 @@ import { NextResponse } from 'next/server'; -import { getFarcasterMetadata } from '../../../lib/utils'; +import { getFarcasterDomainManifest } from '~/lib/utils'; export async function GET() { try { - const config = await getFarcasterMetadata(); + const config = await getFarcasterDomainManifest(); return NextResponse.json(config); } catch (error) { console.error('Error generating metadata:', error); diff --git a/src/components/ui/Share.tsx b/src/components/ui/Share.tsx index ef29ebd..2807032 100644 --- a/src/components/ui/Share.tsx +++ b/src/components/ui/Share.tsx @@ -4,6 +4,7 @@ import { useCallback, useState, useEffect } from 'react'; import { Button } from './Button'; import { useMiniApp } from '@neynar/react'; import { type ComposeCast } from "@farcaster/miniapp-sdk"; +import { APP_URL } from '~/lib/constants'; interface EmbedConfig { path?: string; @@ -72,7 +73,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals return embed; } if (embed.path) { - const baseUrl = process.env.NEXT_PUBLIC_URL || window.location.origin; + const baseUrl = APP_URL || window.location.origin; const url = new URL(`${baseUrl}${embed.path}`); // Add UTM parameters diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index 2f8af39..e7bd99f 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -6,6 +6,7 @@ import { ShareButton } from "../Share"; import { Button } from "../Button"; import { SignIn } from "../wallet/SignIn"; import { type Haptics } from "@farcaster/miniapp-sdk"; +import { APP_URL } from "~/lib/constants"; /** * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. @@ -90,7 +91,7 @@ export function ActionsTab() { */ const copyUserShareUrl = useCallback(async () => { if (context?.user?.fid) { - const userShareUrl = `${process.env.NEXT_PUBLIC_URL}/share/${context.user.fid}`; + const userShareUrl = `${APP_URL}/share/${context.user.fid}`; await navigator.clipboard.writeText(userShareUrl); setNotificationState((prev) => ({ ...prev, shareUrlCopied: true })); setTimeout(() => setNotificationState((prev) => ({ ...prev, shareUrlCopied: false })), 2000); @@ -120,7 +121,7 @@ export function ActionsTab() { cast={{ text: "Check out this awesome frame @1 @2 @3! šŸš€šŸŖ", bestFriends: true, - embeds: [`${process.env.NEXT_PUBLIC_URL}/share/${context?.user?.fid || ''}`] + embeds: [`${APP_URL}/share/${context?.user?.fid || ''}`] }} className="w-full" /> diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6c980be..ddd5536 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,3 +1,5 @@ +import { type AccountAssociation } from '@farcaster/miniapp-node'; + /** * Application constants and configuration values. * @@ -14,63 +16,70 @@ * The base URL of the application. * Used for generating absolute URLs for assets and API endpoints. */ -export const APP_URL = process.env.NEXT_PUBLIC_URL!; +export const APP_URL: string = process.env.NEXT_PUBLIC_URL!; /** * The name of the mini app as displayed to users. * Used in titles, headers, and app store listings. */ -export const APP_NAME = 'Starter Kit'; +export const APP_NAME: string = 'Starter Kit'; /** * A brief description of the mini app's functionality. * Used in app store listings and metadata. */ -export const APP_DESCRIPTION = 'A demo of the Neynar Starter Kit'; +export const APP_DESCRIPTION: string = 'A demo of the Neynar Starter Kit'; /** * The primary category for the mini app. * Used for app store categorization and discovery. */ -export const APP_PRIMARY_CATEGORY = 'developer-tools'; +export const APP_PRIMARY_CATEGORY: string = 'developer-tools'; /** * Tags associated with the mini app. * Used for search and discovery in app stores. */ -export const APP_TAGS = ['neynar', 'starter-kit', 'demo']; +export const APP_TAGS: string[] = ['neynar', 'starter-kit', 'demo']; // --- Asset URLs --- /** * URL for the app's icon image. * Used in app store listings and UI elements. */ -export const APP_ICON_URL = `${APP_URL}/icon.png`; +export const APP_ICON_URL: string = `${APP_URL}/icon.png`; /** * URL for the app's Open Graph image. * Used for social media sharing and previews. */ -export const APP_OG_IMAGE_URL = `${APP_URL}/api/opengraph-image`; +export const APP_OG_IMAGE_URL: string = `${APP_URL}/api/opengraph-image`; /** * URL for the app's splash screen image. * Displayed during app loading. */ -export const APP_SPLASH_URL = `${APP_URL}/splash.png`; +export const APP_SPLASH_URL: string = `${APP_URL}/splash.png`; /** * Background color for the splash screen. * Used as fallback when splash image is loading. */ -export const APP_SPLASH_BACKGROUND_COLOR = "#f7f7f7"; +export const APP_SPLASH_BACKGROUND_COLOR: string = "#f7f7f7"; + +/** + * Account association for the mini app. + * Used to associate the mini app with a Farcaster account. + * If not provided, the mini app will be unsigned and have limited capabilities. + */ +export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = undefined; // --- UI Configuration --- /** * Text displayed on the main action button. * Used for the primary call-to-action in the mini app. */ -export const APP_BUTTON_TEXT = 'Launch NSK'; +export const APP_BUTTON_TEXT: string = 'Launch NSK'; // --- Integration Configuration --- /** @@ -80,7 +89,7 @@ export const APP_BUTTON_TEXT = 'Launch NSK'; * Neynar webhook endpoint. Otherwise, falls back to a local webhook * endpoint for development and testing. */ -export const APP_WEBHOOK_URL = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID +export const APP_WEBHOOK_URL: string = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` : `${APP_URL}/api/webhook`; @@ -91,7 +100,7 @@ export const APP_WEBHOOK_URL = process.env.NEYNAR_API_KEY && process.env.NEYNAR_ * When false, wallet functionality is completely hidden from the UI. * Useful for mini apps that don't require wallet integration. */ -export const USE_WALLET = true; +export const USE_WALLET: boolean = true; /** * Flag to enable/disable analytics tracking. @@ -100,4 +109,4 @@ export const USE_WALLET = true; * When false, analytics collection is disabled. * Useful for privacy-conscious users or development environments. */ -export const ANALYTICS_ENABLED = true; +export const ANALYTICS_ENABLED: boolean = true; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 85168a0..f473387 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,32 +1,19 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; -import { mnemonicToAccount } from 'viem/accounts'; -import { APP_BUTTON_TEXT, APP_DESCRIPTION, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_TAGS, APP_URL, APP_WEBHOOK_URL } from './constants'; -import { APP_SPLASH_URL } from './constants'; - -interface MiniAppMetadata { - version: string; - name: string; - iconUrl: string; - homeUrl: string; - imageUrl?: string; - buttonTitle?: string; - splashImageUrl?: string; - splashBackgroundColor?: string; - webhookUrl?: string; - description?: string; - primaryCategory?: string; - tags?: string[]; -}; - -interface MiniAppManifest { - accountAssociation?: { - header: string; - payload: string; - signature: string; - }; - frame: MiniAppMetadata; -} +import { type Manifest } from '@farcaster/miniapp-node'; +import { + APP_BUTTON_TEXT, + APP_DESCRIPTION, + APP_ICON_URL, + APP_NAME, + APP_OG_IMAGE_URL, + APP_PRIMARY_CATEGORY, + APP_SPLASH_BACKGROUND_COLOR, + APP_SPLASH_URL, + APP_TAGS, APP_URL, + APP_WEBHOOK_URL, + APP_ACCOUNT_ASSOCIATION, +} from './constants'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -64,64 +51,10 @@ export function getMiniAppEmbedMetadata(ogImageUrl?: string) { }; } -export async function getFarcasterMetadata(): Promise { - // First check for MINI_APP_METADATA in .env and use that if it exists - if (process.env.MINI_APP_METADATA) { - try { - const metadata = JSON.parse(process.env.MINI_APP_METADATA); - console.log('Using pre-signed mini app metadata from environment'); - return metadata; - } catch (error) { - console.warn('Failed to parse MINI_APP_METADATA from environment:', error); - } - } - - if (!APP_URL) { - throw new Error('NEXT_PUBLIC_URL not configured'); - } - - // Get the domain from the URL (without https:// prefix) - const domain = new URL(APP_URL).hostname; - console.log('Using domain for manifest:', domain); - - const secretEnvVars = getSecretEnvVars(); - if (!secretEnvVars) { - console.warn('No seed phrase or FID found in environment variables -- generating unsigned metadata'); - } - - let accountAssociation; - if (secretEnvVars) { - // Generate account from seed phrase - const account = mnemonicToAccount(secretEnvVars.seedPhrase); - const custodyAddress = account.address; - - const header = { - fid: parseInt(secretEnvVars.fid), - type: 'custody', - key: custodyAddress, - }; - const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); - - const payload = { - domain - }; - const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); - - const signature = await account.signMessage({ - message: `${encodedHeader}.${encodedPayload}` - }); - const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); - - accountAssociation = { - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature - }; - } - +export async function getFarcasterDomainManifest(): Promise { return { - accountAssociation, - frame: { + accountAssociation: APP_ACCOUNT_ASSOCIATION, + miniapp: { version: "1", name: APP_NAME ?? "Neynar Starter Kit", iconUrl: APP_ICON_URL,