Skip to content

Instantly share code, notes, and snippets.

@fadlee
Last active September 4, 2025 15:25
Show Gist options
  • Save fadlee/0ab1f679565ebd82463f38f426428121 to your computer and use it in GitHub Desktop.
Save fadlee/0ab1f679565ebd82463f38f426428121 to your computer and use it in GitHub Desktop.
Configure private and public key for Convex Auth, submit directly to Convex backend configured in .env.local
// run this using: curl -s https://rawurl... | node
import { generateKeyPair, createPublicKey } from 'crypto';
import { readFile } from 'fs/promises';
import { promisify } from 'util';
import path from 'path';
import { fileURLToPath } from 'url';
import https from 'https';
import http from 'http';
// Get the directory of the current module
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Function to load environment variables from a file
async function loadEnvFile(filePath) {
try {
const content = await readFile(filePath, 'utf8');
const lines = content.split('\n');
for (const line of lines) {
// Skip comments and empty lines
if (!line || line.startsWith('#')) continue;
const [key, ...valueParts] = line.split('=');
const value = valueParts.join('='); // Handle values that might contain = characters
if (key && value) {
process.env[key.trim()] = value.trim();
}
}
} catch (error) {
console.error(`Error loading env file: ${error.message}`);
}
}
// Load environment variables from .env.local
await loadEnvFile(path.join(__dirname, '.env.local'));
// Promisify the generateKeyPair function from crypto
const generateKeyPairAsync = promisify(generateKeyPair);
// Generate RSA key pair
const { privateKey, publicKey } = await generateKeyPairAsync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
// Convert public key to JWK format using Node.js crypto module
// Parse the PEM public key
const pubKeyObj = createPublicKey(publicKey);
// Export as JWK
const jwk = pubKeyObj.export({ format: 'jwk' });
// Add the 'use' property for JWT signing
jwk.use = 'sig';
// Create the JWKS
const jwks = JSON.stringify({ keys: [jwk] });
// Format the private key for storage
const formattedPrivateKey = privateKey.trimEnd().replace(/\n/g, " ");
// Get environment variables
const baseUrl = process.env.CONVEX_SELF_HOSTED_URL;
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY;
if (!baseUrl || !adminKey) {
console.error('Missing required environment variables. Please check your .env.local file.');
process.exit(1);
}
// Prepare the payload
const payload = {
changes: [
{
name: "JWT_PRIVATE_KEY",
value: formattedPrivateKey
},
{
name: "JWKS",
value: jwks
}
]
};
// Log what we're about to do
console.log(`Submitting keys to ${baseUrl}/api/update_environment_variables`);
// Send the request
try {
console.log('Request payload:', JSON.stringify(payload, null, 2));
// Function to make HTTP/HTTPS requests
function makeRequest(url, options, data) {
return new Promise((resolve, reject) => {
const isHttps = url.startsWith('https');
const requestLib = isHttps ? https : http;
const urlObj = new URL(url);
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = requestLib.request(requestOptions, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
resolve({
status: res.statusCode,
headers: res.headers,
text: () => Promise.resolve(responseData),
json: () => {
try {
return Promise.resolve(JSON.parse(responseData));
} catch (error) {
return Promise.reject(new Error('Invalid JSON response'));
}
},
ok: res.statusCode >= 200 && res.statusCode < 300
});
});
});
req.on('error', (error) => {
reject(error);
});
if (data) {
req.write(data);
}
req.end();
});
}
// Make the request
const response = await makeRequest(`${baseUrl}/api/update_environment_variables`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Convex ${adminKey}`
}
}, JSON.stringify(payload));
console.log('Response status:', response.status);
console.log('Response headers:', JSON.stringify(response.headers, null, 2));
// Get the response as text first
const responseText = await response.text();
console.log('Response body (raw):', responseText);
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${responseText}`);
}
// Try to parse as JSON if possible
let data;
try {
if (responseText.trim()) {
data = JSON.parse(responseText);
console.log('Response data (parsed):', data);
} else {
console.log('Response was empty but status was OK');
}
} catch (parseError) {
console.log('Could not parse response as JSON, but status was OK');
}
console.log('Keys successfully submitted to Convex API');
} catch (error) {
console.error('Error submitting keys to Convex API:', error.message);
process.exit(1);
}
// Also output to console for reference
console.log('\nGenerated keys:');
console.log(`JWT_PRIVATE_KEY="${formattedPrivateKey}"`);
console.log(`JWKS=${jwks}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment