Browse Source

Sync from gitrepublic-web monorepo

master
Silberengel 3 weeks ago
parent
commit
74bea82ada
  1. 27
      scripts/commands/pushAll.js
  2. 23
      scripts/git-commit-msg-hook.js
  3. 13
      scripts/git-wrapper.js
  4. 38
      scripts/setup.js
  5. 20
      scripts/uninstall.js
  6. 7
      scripts/utils/event-storage.js

27
scripts/commands/pushAll.js

@ -1,7 +1,8 @@
import { execSync } from 'child_process'; // Note: Using spawn instead of execSync for security (prevents command injection)
/** /**
* Push to all remotes * Push to all remotes
* Security: Uses spawn with argument arrays to prevent command injection
*/ */
export async function pushAll(args, server, json) { export async function pushAll(args, server, json) {
// Check for help flag // Check for help flag
@ -47,9 +48,20 @@ Notes:
const dryRun = args.includes('--dry-run') || args.includes('-n'); const dryRun = args.includes('--dry-run') || args.includes('-n');
// Get all remotes // Get all remotes
// Security: Use spawn with argument arrays to prevent command injection
let remotes = []; let remotes = [];
try { try {
const remoteOutput = execSync('git remote', { encoding: 'utf-8' }).trim(); const { spawn } = await import('child_process');
const remoteOutput = await new Promise((resolve, reject) => {
const proc = spawn('git', ['remote'], { encoding: 'utf-8' });
let output = '';
proc.stdout.on('data', (chunk) => { output += chunk.toString(); });
proc.on('close', (code) => {
if (code === 0) resolve(output.trim());
else reject(new Error(`git remote exited with code ${code}`));
});
proc.on('error', reject);
});
remotes = remoteOutput.split('\n').filter(r => r.trim()); remotes = remoteOutput.split('\n').filter(r => r.trim());
} catch (err) { } catch (err) {
console.error('Error: Not in a git repository or unable to read remotes'); console.error('Error: Not in a git repository or unable to read remotes');
@ -85,12 +97,21 @@ Notes:
console.log(`\nPushing to ${remote}...`); console.log(`\nPushing to ${remote}...`);
} }
// Security: Use spawn with argument arrays to prevent command injection
const { spawn } = await import('child_process');
const command = ['push', remote, ...pushArgs]; const command = ['push', remote, ...pushArgs];
execSync(`git ${command.join(' ')}`, { await new Promise((resolve, reject) => {
const proc = spawn('git', command, {
stdio: json ? 'pipe' : 'inherit', stdio: json ? 'pipe' : 'inherit',
encoding: 'utf-8' encoding: 'utf-8'
}); });
proc.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`git push exited with code ${code}`));
});
proc.on('error', reject);
});
results.push({ remote, status: 'success' }); results.push({ remote, status: 'success' });
successCount++; successCount++;

23
scripts/git-commit-msg-hook.js

@ -33,8 +33,8 @@
import { finalizeEvent, getPublicKey, nip19 } from 'nostr-tools'; import { finalizeEvent, getPublicKey, nip19 } from 'nostr-tools';
import { publishToRelays } from './relay/publisher.js'; import { publishToRelays } from './relay/publisher.js';
import { enhanceRelayList } from './relay/relay-fetcher.js'; import { enhanceRelayList } from './relay/relay-fetcher.js';
import { readFileSync, writeFileSync, existsSync } from 'fs'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { execSync } from 'child_process'; import { spawnSync } from 'child_process';
import { join, dirname, resolve } from 'path'; import { join, dirname, resolve } from 'path';
// Commit signature event kind (1640) // Commit signature event kind (1640)
@ -76,10 +76,18 @@ function decodeNostrKey(key) {
/** /**
* Get git config value * Get git config value
* Security: Validates key to prevent command injection
*/ */
function getGitConfig(key) { function getGitConfig(key) {
try { try {
return execSync(`git config --get ${key}`, { encoding: 'utf-8' }).trim() || null; // Validate key to prevent injection (git config keys are alphanumeric with dots and hyphens)
if (!key || typeof key !== 'string' || !/^[a-zA-Z0-9.-]+$/.test(key)) {
return null;
}
// Security: Use spawnSync with argument array instead of string concatenation
const result = spawnSync('git', ['config', '--get', key], { encoding: 'utf-8' });
if (result.status !== 0) return null;
return result.stdout.trim() || null;
} catch { } catch {
return null; return null;
} }
@ -94,8 +102,10 @@ function getGitConfig(key) {
function isGitRepublicRepo() { function isGitRepublicRepo() {
try { try {
// Get all remotes // Get all remotes
const remotes = execSync('git remote -v', { encoding: 'utf-8' }); // Security: Use spawnSync with argument arrays
const remoteLines = remotes.split('\n').filter(line => line.trim()); const result = spawnSync('git', ['remote', '-v'], { encoding: 'utf-8' });
if (result.status !== 0) return false;
const remoteLines = result.stdout.split('\n').filter(line => line.trim());
// Check if any remote URL matches GitRepublic patterns // Check if any remote URL matches GitRepublic patterns
// GitRepublic URLs use specific path patterns to distinguish from GRASP: // GitRepublic URLs use specific path patterns to distinguish from GRASP:
@ -303,7 +313,8 @@ async function signCommitMessage(commitMessageFile) {
// Store in nostr/ folder in repository root // Store in nostr/ folder in repository root
const nostrDir = join(repoRoot, 'nostr'); const nostrDir = join(repoRoot, 'nostr');
if (!existsSync(nostrDir)) { if (!existsSync(nostrDir)) {
execSync(`mkdir -p "${nostrDir}"`, { stdio: 'ignore' }); // Security: Use fs.mkdirSync instead of execSync for path safety
mkdirSync(nostrDir, { recursive: true });
} }
// Append to commit-signatures.jsonl (JSON Lines format) // Append to commit-signatures.jsonl (JSON Lines format)

13
scripts/git-wrapper.js

@ -21,7 +21,7 @@
* gitrep publish <subcommand> * gitrep publish <subcommand>
*/ */
import { spawn, execSync } from 'child_process'; import { spawn, spawnSync } from 'child_process';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { finalizeEvent } from 'nostr-tools'; import { finalizeEvent } from 'nostr-tools';
import { decode } from 'nostr-tools/nip19'; import { decode } from 'nostr-tools/nip19';
@ -51,10 +51,17 @@ const API_COMMANDS = [
]; ];
// Get git remote URL // Get git remote URL
// Security: Validates remote name to prevent command injection
function getRemoteUrl(remote = 'origin') { function getRemoteUrl(remote = 'origin') {
try { try {
const url = execSync(`git config --get remote.${remote}.url`, { encoding: 'utf-8' }).trim(); // Validate remote name to prevent injection
return url; if (!remote || typeof remote !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(remote)) {
return null;
}
// Security: Use spawnSync with argument array instead of string concatenation
const result = spawnSync('git', ['config', '--get', `remote.${remote}.url`], { encoding: 'utf-8' });
if (result.status !== 0) return null;
return result.stdout.trim();
} catch { } catch {
return null; return null;
} }

38
scripts/setup.js

@ -16,8 +16,8 @@
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path'; import { dirname, join, resolve } from 'path';
import { existsSync } from 'fs'; import { existsSync, mkdirSync, unlinkSync, symlinkSync } from 'fs';
import { execSync } from 'child_process'; import { spawnSync } from 'child_process';
// Get the directory where this script is located // Get the directory where this script is located
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -108,19 +108,25 @@ function setupCredentialHelper() {
try { try {
let configCommand; let configCommand;
// Security: Use spawnSync with argument arrays to prevent command injection
if (domain) { if (domain) {
// Configure for specific domain // Configure for specific domain
// Validate domain to prevent injection
const protocol = domain.startsWith('https://') ? 'https' : domain.startsWith('http://') ? 'http' : 'https'; const protocol = domain.startsWith('https://') ? 'https' : domain.startsWith('http://') ? 'http' : 'https';
const host = domain.replace(/^https?:\/\//, '').split('/')[0]; const host = domain.replace(/^https?:\/\//, '').split('/')[0];
configCommand = `git config --global credential.${protocol}://${host}.helper '!node ${credentialScript}'`; // Validate host format (basic check)
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
throw new Error('Invalid domain format');
}
const configKey = `credential.${protocol}://${host}.helper`;
const configValue = `!node ${credentialScript}`;
spawnSync('git', ['config', '--global', configKey, configValue], { stdio: 'inherit' });
console.log(` Configuring for domain: ${host}`); console.log(` Configuring for domain: ${host}`);
} else { } else {
// Configure globally for all domains // Configure globally for all domains
configCommand = `git config --global credential.helper '!node ${credentialScript}'`; spawnSync('git', ['config', '--global', 'credential.helper', `!node ${credentialScript}`], { stdio: 'inherit' });
console.log(' Configuring globally for all domains'); console.log(' Configuring globally for all domains');
} }
execSync(configCommand, { stdio: 'inherit' });
console.log('✅ Credential helper configured successfully!\n'); console.log('✅ Credential helper configured successfully!\n');
} catch (error) { } catch (error) {
console.error('❌ Failed to configure credential helper:', error.message); console.error('❌ Failed to configure credential helper:', error.message);
@ -138,21 +144,24 @@ function setupCommitHook() {
const hooksDir = resolve(process.env.HOME, '.git-hooks'); const hooksDir = resolve(process.env.HOME, '.git-hooks');
// Create hooks directory if it doesn't exist // Create hooks directory if it doesn't exist
// Security: Use fs.mkdirSync instead of execSync
if (!existsSync(hooksDir)) { if (!existsSync(hooksDir)) {
execSync(`mkdir -p "${hooksDir}"`, { stdio: 'inherit' }); mkdirSync(hooksDir, { recursive: true });
} }
// Create symlink // Create symlink
const hookPath = join(hooksDir, 'commit-msg'); const hookPath = join(hooksDir, 'commit-msg');
if (existsSync(hookPath)) { if (existsSync(hookPath)) {
console.log(' Removing existing hook...'); console.log(' Removing existing hook...');
execSync(`rm "${hookPath}"`, { stdio: 'inherit' }); unlinkSync(hookPath);
} }
execSync(`ln -s "${commitHookScript}" "${hookPath}"`, { stdio: 'inherit' }); // Security: Use fs.symlinkSync instead of execSync
symlinkSync(commitHookScript, hookPath);
// Configure git to use global hooks // Configure git to use global hooks
execSync('git config --global core.hooksPath ~/.git-hooks', { stdio: 'inherit' }); // Note: Using ~/.git-hooks is safe as it's a literal string, not user input
spawnSync('git', ['config', '--global', 'core.hooksPath', '~/.git-hooks'], { stdio: 'inherit' });
console.log('✅ Commit hook installed globally for all repositories!\n'); console.log('✅ Commit hook installed globally for all repositories!\n');
} else { } else {
@ -166,18 +175,20 @@ function setupCommitHook() {
const hookPath = join(gitDir, 'hooks', 'commit-msg'); const hookPath = join(gitDir, 'hooks', 'commit-msg');
// Create hooks directory if it doesn't exist // Create hooks directory if it doesn't exist
// Security: Use fs.mkdirSync instead of execSync
const hooksDir = join(gitDir, 'hooks'); const hooksDir = join(gitDir, 'hooks');
if (!existsSync(hooksDir)) { if (!existsSync(hooksDir)) {
execSync(`mkdir -p "${hooksDir}"`, { stdio: 'inherit' }); mkdirSync(hooksDir, { recursive: true });
} }
// Create symlink // Create symlink
// Security: Use fs operations instead of execSync
if (existsSync(hookPath)) { if (existsSync(hookPath)) {
console.log(' Removing existing hook...'); console.log(' Removing existing hook...');
execSync(`rm "${hookPath}"`, { stdio: 'inherit' }); unlinkSync(hookPath);
} }
execSync(`ln -s "${commitHookScript}" "${hookPath}"`, { stdio: 'inherit' }); symlinkSync(commitHookScript, hookPath);
console.log('✅ Commit hook installed for current repository!\n'); console.log('✅ Commit hook installed for current repository!\n');
} }
@ -235,3 +246,4 @@ if (!secretKey) {
} }
console.log('2. Test credential helper: gitrep clone <gitrepublic-repo-url> gitrepublic-web'); console.log('2. Test credential helper: gitrep clone <gitrepublic-repo-url> gitrepublic-web');
console.log('3. Test commit signing: gitrep commit -m "Test commit"'); console.log('3. Test commit signing: gitrep commit -m "Test commit"');

20
scripts/uninstall.js

@ -5,7 +5,7 @@
* Removes all GitRepublic CLI configuration from your system * Removes all GitRepublic CLI configuration from your system
*/ */
import { execSync } from 'child_process'; import { spawnSync } from 'child_process';
import { existsSync, unlinkSync, rmdirSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, unlinkSync, rmdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { homedir } from 'os'; import { homedir } from 'os';
@ -96,9 +96,12 @@ function main() {
// Remove credential helper configurations // Remove credential helper configurations
console.log('Removing git credential helper configurations...'); console.log('Removing git credential helper configurations...');
try { try {
const credentialConfigs = execSync('git config --global --get-regexp credential.*helper', { encoding: 'utf-8' }) // Security: Use spawnSync with argument arrays
const result = spawnSync('git', ['config', '--global', '--get-regexp', 'credential.*helper'], { encoding: 'utf-8' });
if (result.status === 0) {
const credentialConfigs = result.stdout
.split('\n') .split('\n')
.filter(line => line.trim() && line.includes('gitrepublic') || line.includes('git-credential-nostr')); .filter(line => line.trim() && (line.includes('gitrepublic') || line.includes('git-credential-nostr')));
for (const config of credentialConfigs) { for (const config of credentialConfigs) {
if (config.trim()) { if (config.trim()) {
@ -107,7 +110,8 @@ function main() {
console.log(` - ${key}`); console.log(` - ${key}`);
if (!dryRun) { if (!dryRun) {
try { try {
execSync(`git config --global --unset "${key}"`, { stdio: 'ignore' }); // Security: Use spawnSync with argument arrays
spawnSync('git', ['config', '--global', '--unset', key], { stdio: 'ignore' });
} catch { } catch {
// Ignore if already removed // Ignore if already removed
} }
@ -116,6 +120,7 @@ function main() {
} }
} }
} }
}
} catch { } catch {
// No credential helpers configured // No credential helpers configured
} }
@ -123,7 +128,9 @@ function main() {
// Remove commit hook (global) // Remove commit hook (global)
console.log('\nRemoving global commit hook...'); console.log('\nRemoving global commit hook...');
try { try {
const hooksPath = execSync('git config --global --get core.hooksPath', { encoding: 'utf-8' }).trim(); // Security: Use spawnSync with argument arrays
const result = spawnSync('git', ['config', '--global', '--get', 'core.hooksPath'], { encoding: 'utf-8' });
const hooksPath = result.status === 0 ? result.stdout.trim() : null;
if (hooksPath) { if (hooksPath) {
const hookFile = join(hooksPath, 'commit-msg'); const hookFile = join(hooksPath, 'commit-msg');
if (existsSync(hookFile)) { if (existsSync(hookFile)) {
@ -147,7 +154,8 @@ function main() {
// Remove core.hooksPath config // Remove core.hooksPath config
try { try {
execSync('git config --global --unset core.hooksPath', { stdio: 'ignore' }); // Security: Use spawnSync with argument arrays
spawnSync('git', ['config', '--global', '--unset', 'core.hooksPath'], { stdio: 'ignore' });
if (!dryRun) { if (!dryRun) {
console.log(' - Removed core.hooksPath configuration'); console.log(' - Removed core.hooksPath configuration');
} }

7
scripts/utils/event-storage.js

@ -1,9 +1,9 @@
import { writeFileSync, existsSync } from 'fs'; import { writeFileSync, existsSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
/** /**
* Store event in appropriate JSONL file based on event kind * Store event in appropriate JSONL file based on event kind
* Security: Uses fs.mkdirSync instead of execSync to prevent command injection
*/ */
export function storeEventInJsonl(event) { export function storeEventInJsonl(event) {
try { try {
@ -28,9 +28,10 @@ export function storeEventInJsonl(event) {
} }
// Create nostr/ directory if it doesn't exist // Create nostr/ directory if it doesn't exist
// Security: Use fs.mkdirSync instead of execSync for path safety
const nostrDir = join(repoRoot, 'nostr'); const nostrDir = join(repoRoot, 'nostr');
if (!existsSync(nostrDir)) { if (!existsSync(nostrDir)) {
execSync(`mkdir -p "${nostrDir}"`, { stdio: 'ignore' }); mkdirSync(nostrDir, { recursive: true });
} }
// Determine JSONL file name based on event kind // Determine JSONL file name based on event kind

Loading…
Cancel
Save