@gannochenko/staticstripes 0.0.11 → 0.0.14
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/Makefile +37 -4
- package/dist/asset-manager.d.ts +1 -0
- package/dist/asset-manager.d.ts.map +1 -1
- package/dist/asset-manager.js +3 -0
- package/dist/asset-manager.js.map +1 -1
- package/dist/cli/ai-generation-strategy-factory.d.ts +23 -0
- package/dist/cli/ai-generation-strategy-factory.d.ts.map +1 -0
- package/dist/cli/ai-generation-strategy-factory.js +44 -0
- package/dist/cli/ai-generation-strategy-factory.js.map +1 -0
- package/dist/cli/ai-generation-strategy.d.ts +33 -0
- package/dist/cli/ai-generation-strategy.d.ts.map +1 -0
- package/dist/cli/ai-generation-strategy.js +3 -0
- package/dist/cli/ai-generation-strategy.js.map +1 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +38 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +174 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -0
- package/dist/cli/auth-strategy-factory.d.ts +31 -0
- package/dist/cli/auth-strategy-factory.d.ts.map +1 -0
- package/dist/cli/auth-strategy-factory.js +61 -0
- package/dist/cli/auth-strategy-factory.js.map +1 -0
- package/dist/cli/auth-strategy.d.ts +31 -0
- package/dist/cli/auth-strategy.d.ts.map +1 -0
- package/dist/cli/auth-strategy.js +3 -0
- package/dist/cli/auth-strategy.js.map +1 -0
- package/dist/cli/commands/add-assets.d.ts +3 -0
- package/dist/cli/commands/add-assets.d.ts.map +1 -0
- package/dist/cli/commands/add-assets.js +113 -0
- package/dist/cli/commands/add-assets.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +103 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +49 -0
- package/dist/cli/commands/bootstrap.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +3 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +199 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/upload.d.ts +6 -0
- package/dist/cli/commands/upload.d.ts.map +1 -0
- package/dist/cli/commands/upload.js +67 -0
- package/dist/cli/commands/upload.js.map +1 -0
- package/dist/cli/instagram/instagram-auth-strategy.d.ts +31 -0
- package/dist/cli/instagram/instagram-auth-strategy.d.ts.map +1 -0
- package/dist/cli/instagram/instagram-auth-strategy.js +505 -0
- package/dist/cli/instagram/instagram-auth-strategy.js.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts +45 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.js +303 -0
- package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -0
- package/dist/cli/s3/s3-upload-strategy.d.ts +18 -0
- package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -0
- package/dist/cli/s3/s3-upload-strategy.js +153 -0
- package/dist/cli/s3/s3-upload-strategy.js.map +1 -0
- package/dist/cli/upload-strategy-factory.d.ts +23 -0
- package/dist/cli/upload-strategy-factory.d.ts.map +1 -0
- package/dist/cli/upload-strategy-factory.js +49 -0
- package/dist/cli/upload-strategy-factory.js.map +1 -0
- package/dist/cli/upload-strategy.d.ts +25 -0
- package/dist/cli/upload-strategy.d.ts.map +1 -0
- package/dist/cli/upload-strategy.js +3 -0
- package/dist/cli/upload-strategy.js.map +1 -0
- package/dist/cli/youtube/youtube-auth-strategy.d.ts +11 -0
- package/dist/cli/youtube/youtube-auth-strategy.d.ts.map +1 -0
- package/dist/cli/youtube/youtube-auth-strategy.js +320 -0
- package/dist/cli/youtube/youtube-auth-strategy.js.map +1 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts +22 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -0
- package/dist/cli/youtube/youtube-upload-strategy.js +117 -0
- package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -0
- package/dist/cli.js +11 -281
- package/dist/cli.js.map +1 -1
- package/dist/html-parser.d.ts +3 -4
- package/dist/html-parser.d.ts.map +1 -1
- package/dist/html-parser.js +20 -17
- package/dist/html-parser.js.map +1 -1
- package/dist/html-project-parser.d.ts +64 -1
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +695 -57
- package/dist/html-project-parser.js.map +1 -1
- package/dist/lib/file.d.ts +2 -0
- package/dist/lib/file.d.ts.map +1 -0
- package/dist/lib/file.js +13 -0
- package/dist/lib/file.js.map +1 -0
- package/dist/lib/net.d.ts +19 -0
- package/dist/lib/net.d.ts.map +1 -0
- package/dist/lib/net.js +101 -0
- package/dist/lib/net.js.map +1 -0
- package/dist/project.d.ts +18 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +65 -1
- package/dist/project.js.map +1 -1
- package/dist/type.d.ts +43 -4
- package/dist/type.d.ts.map +1 -1
- package/dist/youtube-uploader.d.ts +40 -0
- package/dist/youtube-uploader.d.ts.map +1 -0
- package/dist/youtube-uploader.js +227 -0
- package/dist/youtube-uploader.js.map +1 -0
- package/package.json +6 -2
- package/src/asset-manager.ts +4 -0
- package/src/cli/ai-generation-strategy-factory.ts +48 -0
- package/src/cli/ai-generation-strategy.ts +35 -0
- package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +266 -0
- package/src/cli/auth-strategy-factory.ts +67 -0
- package/src/cli/auth-strategy.ts +37 -0
- package/src/cli/commands/add-assets.ts +159 -0
- package/src/cli/commands/auth.ts +120 -0
- package/src/cli/commands/bootstrap.ts +57 -0
- package/src/cli/commands/generate.ts +242 -0
- package/src/cli/commands/upload.ts +83 -0
- package/src/cli/instagram/instagram-auth-strategy.ts +569 -0
- package/src/cli/instagram/instagram-upload-strategy.ts +398 -0
- package/src/cli/s3/s3-upload-strategy.ts +198 -0
- package/src/cli/upload-strategy-factory.ts +55 -0
- package/src/cli/upload-strategy.ts +31 -0
- package/src/cli/youtube/youtube-auth-strategy.ts +323 -0
- package/src/cli/youtube/youtube-upload-strategy.ts +174 -0
- package/src/cli.ts +13 -391
- package/src/html-parser.ts +23 -21
- package/src/html-project-parser.ts +821 -62
- package/src/lib/file.ts +11 -0
- package/src/lib/net.ts +120 -0
- package/src/project.ts +81 -1
- package/src/type.ts +49 -4
- package/src/youtube-uploader.ts +288 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { HTMLParser } from '../../html-parser.js';
|
|
5
|
+
import { HTMLProjectParser } from '../../html-project-parser.js';
|
|
6
|
+
import { UploadStrategyFactory } from '../upload-strategy-factory.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registers the generic upload command that works with any upload provider
|
|
10
|
+
*/
|
|
11
|
+
export function registerUploadCommand(
|
|
12
|
+
program: Command,
|
|
13
|
+
handleError: (error: any, operation: string) => void,
|
|
14
|
+
): void {
|
|
15
|
+
program
|
|
16
|
+
.command('upload')
|
|
17
|
+
.description('Upload video to configured platform (YouTube, S3, etc.)')
|
|
18
|
+
.option('-p, --project <path>', 'Path to project directory', '.')
|
|
19
|
+
.requiredOption('--upload-name <name>', 'Name of the upload configuration')
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
// Resolve project path
|
|
23
|
+
const projectPath = resolve(process.cwd(), options.project);
|
|
24
|
+
const projectFilePath = resolve(projectPath, 'project.html');
|
|
25
|
+
|
|
26
|
+
// Validate project.html exists
|
|
27
|
+
if (!existsSync(projectFilePath)) {
|
|
28
|
+
console.error(`❌ Error: project.html not found in ${projectPath}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`📁 Project: ${projectPath}`);
|
|
33
|
+
console.log(`📄 Loading: ${projectFilePath}\n`);
|
|
34
|
+
|
|
35
|
+
// Parse the project HTML file
|
|
36
|
+
const parser = new HTMLProjectParser(
|
|
37
|
+
await new HTMLParser().parseFile(projectFilePath),
|
|
38
|
+
projectFilePath,
|
|
39
|
+
);
|
|
40
|
+
const project = await parser.parse();
|
|
41
|
+
|
|
42
|
+
// Get the upload configuration
|
|
43
|
+
const upload = project.getUpload(options.uploadName);
|
|
44
|
+
if (!upload) {
|
|
45
|
+
const availableUploads = Array.from(
|
|
46
|
+
project.getUploads().keys(),
|
|
47
|
+
);
|
|
48
|
+
console.error(
|
|
49
|
+
`❌ Upload "${options.uploadName}" not found in project.html\n`,
|
|
50
|
+
);
|
|
51
|
+
if (availableUploads.length > 0) {
|
|
52
|
+
console.error(`Available uploads: ${availableUploads.join(', ')}`);
|
|
53
|
+
} else {
|
|
54
|
+
console.error('No uploads defined in project.html');
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate output file exists
|
|
60
|
+
const output = project.getOutput(upload.outputName);
|
|
61
|
+
if (output && !existsSync(output.path)) {
|
|
62
|
+
console.error(`❌ Error: Output file not found: ${output.path}`);
|
|
63
|
+
console.error(
|
|
64
|
+
'💡 Please generate the video first with: staticstripes generate\n',
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get the appropriate strategy for this upload tag
|
|
70
|
+
const factory = UploadStrategyFactory.createDefault();
|
|
71
|
+
const strategy = factory.getStrategy(upload.tag);
|
|
72
|
+
|
|
73
|
+
// Validate strategy requirements
|
|
74
|
+
strategy.validate();
|
|
75
|
+
|
|
76
|
+
// Execute the upload
|
|
77
|
+
await strategy.execute(project, upload, projectPath);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
handleError(error, 'Upload');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { AuthStrategy, AuthOptions } from '../auth-strategy';
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
import { parse as parseUrl } from 'url';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Instagram authentication strategy
|
|
11
|
+
* Automatic OAuth flow with browser redirect (like YouTube)
|
|
12
|
+
*/
|
|
13
|
+
export class InstagramAuthStrategy implements AuthStrategy {
|
|
14
|
+
getTag(): string {
|
|
15
|
+
return 'instagram';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async execute(
|
|
19
|
+
uploadName: string,
|
|
20
|
+
projectPath: string,
|
|
21
|
+
options?: AuthOptions,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
console.log(`🔐 Instagram Authentication Setup\n`);
|
|
24
|
+
|
|
25
|
+
const rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const question = (prompt: string): Promise<string> => {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(prompt, (answer) => {
|
|
33
|
+
resolve(answer);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
console.log('━'.repeat(60));
|
|
40
|
+
console.log('STEP 1: Enter Instagram App Credentials');
|
|
41
|
+
console.log('━'.repeat(60));
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log('💡 Run `staticstripes auth-help instagram` for setup instructions\n');
|
|
44
|
+
|
|
45
|
+
const appId = await question('Enter your Instagram App ID: ');
|
|
46
|
+
if (!appId || appId.trim().length < 5) {
|
|
47
|
+
throw new Error('Invalid App ID');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const appSecret = await question('Enter your Instagram App Secret: ');
|
|
51
|
+
if (!appSecret || appSecret.trim().length < 10) {
|
|
52
|
+
throw new Error('Invalid App Secret');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Use provided redirect URL or default to localhost
|
|
56
|
+
const redirectUri =
|
|
57
|
+
options?.oauthRedirectUrl || 'http://localhost:3000/oauth2callback';
|
|
58
|
+
|
|
59
|
+
console.log(`\n🔗 Using OAuth Redirect URI: ${redirectUri}`);
|
|
60
|
+
if (!redirectUri.includes('localhost')) {
|
|
61
|
+
console.log('✅ Using external URL (ngrok/Cloudflare)');
|
|
62
|
+
} else {
|
|
63
|
+
console.log(
|
|
64
|
+
'⚠️ Using localhost - this may not work with Instagram. Consider using --oauth-redirect-url with ngrok/Cloudflare',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('\n━'.repeat(60));
|
|
69
|
+
console.log('STEP 2: Authorize with Instagram');
|
|
70
|
+
console.log('━'.repeat(60));
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
rl.close();
|
|
74
|
+
|
|
75
|
+
console.log('🌐 Starting local server on http://localhost:3000...\n');
|
|
76
|
+
|
|
77
|
+
// Wait for OAuth callback
|
|
78
|
+
const authCode = await this.waitForAuthCode(
|
|
79
|
+
appId.trim(),
|
|
80
|
+
redirectUri.trim(),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
console.log('🔑 Authorization code received\n');
|
|
84
|
+
console.log('🔄 Exchanging for access token...\n');
|
|
85
|
+
|
|
86
|
+
// Exchange code for short-lived token
|
|
87
|
+
const shortLivedToken = await this.exchangeCodeForToken(
|
|
88
|
+
authCode,
|
|
89
|
+
appId.trim(),
|
|
90
|
+
appSecret.trim(),
|
|
91
|
+
redirectUri.trim(),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
console.log('✅ Short-lived token received\n');
|
|
95
|
+
console.log('🔄 Exchanging for long-lived token (60 days)...\n');
|
|
96
|
+
|
|
97
|
+
// Exchange for long-lived token
|
|
98
|
+
const longLivedToken = await this.exchangeForLongLivedToken(
|
|
99
|
+
shortLivedToken,
|
|
100
|
+
appSecret.trim(),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
console.log('✅ Long-lived token received\n');
|
|
104
|
+
console.log('🔍 Fetching Instagram account info...\n');
|
|
105
|
+
|
|
106
|
+
// Get Instagram user ID
|
|
107
|
+
const { id, username } = await this.getInstagramUserId(longLivedToken);
|
|
108
|
+
|
|
109
|
+
console.log(`✅ Account: @${username}`);
|
|
110
|
+
console.log(`✅ Instagram User ID: ${id}\n`);
|
|
111
|
+
console.log('💾 Saving credentials...\n');
|
|
112
|
+
|
|
113
|
+
// Save credentials
|
|
114
|
+
const authDir = resolve(projectPath, '.auth');
|
|
115
|
+
if (!existsSync(authDir)) {
|
|
116
|
+
mkdirSync(authDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const credentialsPath = resolve(authDir, `${uploadName}.json`);
|
|
120
|
+
const credentials = {
|
|
121
|
+
appId: appId.trim(),
|
|
122
|
+
appSecret: appSecret.trim(),
|
|
123
|
+
accessToken: longLivedToken,
|
|
124
|
+
igUserId: id,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
writeFileSync(
|
|
128
|
+
credentialsPath,
|
|
129
|
+
JSON.stringify(credentials, null, 2),
|
|
130
|
+
'utf-8',
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
console.log(`✅ Authentication complete for ${uploadName}!\n`);
|
|
134
|
+
console.log(`📁 Credentials saved to: ${credentialsPath}\n`);
|
|
135
|
+
console.log('⚠️ Token expires in 60 days - set a reminder to refresh!\n');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generates Instagram OAuth authorization URL
|
|
143
|
+
*/
|
|
144
|
+
private getAuthUrl(appId: string, redirectUri: string): string {
|
|
145
|
+
const params = new URLSearchParams({
|
|
146
|
+
client_id: appId,
|
|
147
|
+
redirect_uri: redirectUri,
|
|
148
|
+
scope: 'instagram_business_basic,instagram_business_content_publish',
|
|
149
|
+
response_type: 'code',
|
|
150
|
+
state: Math.random().toString(36).substring(7),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return `https://api.instagram.com/oauth/authorize?${params.toString()}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Starts local HTTP server and waits for OAuth callback
|
|
158
|
+
*/
|
|
159
|
+
private async waitForAuthCode(
|
|
160
|
+
appId: string,
|
|
161
|
+
redirectUri: string,
|
|
162
|
+
): Promise<string> {
|
|
163
|
+
return new Promise<string>((resolve, reject) => {
|
|
164
|
+
const connections = new Set<any>();
|
|
165
|
+
|
|
166
|
+
const server = http.createServer((req, res) => {
|
|
167
|
+
const url = parseUrl(req.url || '', true);
|
|
168
|
+
|
|
169
|
+
if (url.pathname === '/oauth2callback') {
|
|
170
|
+
const code = url.query.code as string;
|
|
171
|
+
const error = url.query.error as string;
|
|
172
|
+
|
|
173
|
+
const closeServer = () => {
|
|
174
|
+
connections.forEach((socket) => socket.destroy());
|
|
175
|
+
connections.clear();
|
|
176
|
+
server.close();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (error) {
|
|
180
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
181
|
+
res.end(`
|
|
182
|
+
<html>
|
|
183
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
184
|
+
<h1>❌ Authorization Failed</h1>
|
|
185
|
+
<p>Error: ${error}</p>
|
|
186
|
+
<p>${url.query.error_description || ''}</p>
|
|
187
|
+
<p>You can close this window.</p>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
`);
|
|
191
|
+
res.on('finish', closeServer);
|
|
192
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (code) {
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
198
|
+
res.end(`
|
|
199
|
+
<html>
|
|
200
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
201
|
+
<h1>Authorization Successful!</h1>
|
|
202
|
+
<p>You can close this window and return to the terminal.</p>
|
|
203
|
+
</body>
|
|
204
|
+
</html>
|
|
205
|
+
`);
|
|
206
|
+
res.on('finish', closeServer);
|
|
207
|
+
resolve(code);
|
|
208
|
+
} else {
|
|
209
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
210
|
+
res.end(`
|
|
211
|
+
<html>
|
|
212
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
213
|
+
<h1>❌ No Authorization Code</h1>
|
|
214
|
+
<p>No code was received from Instagram.</p>
|
|
215
|
+
<p>You can close this window.</p>
|
|
216
|
+
</body>
|
|
217
|
+
</html>
|
|
218
|
+
`);
|
|
219
|
+
res.on('finish', closeServer);
|
|
220
|
+
reject(new Error('No authorization code received'));
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
res.writeHead(404);
|
|
224
|
+
res.end('Not found');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
server.on('connection', (socket) => {
|
|
229
|
+
connections.add(socket);
|
|
230
|
+
socket.on('close', () => connections.delete(socket));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
server.listen(3000, async () => {
|
|
234
|
+
console.log('✅ Server started successfully\n');
|
|
235
|
+
console.log(
|
|
236
|
+
`🌐 Opening browser for authorization, redirect url = ${redirectUri}\n`,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const authUrl = this.getAuthUrl(appId, redirectUri);
|
|
240
|
+
try {
|
|
241
|
+
await open(authUrl);
|
|
242
|
+
console.log('✅ Browser opened successfully\n');
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.log('⚠️ Could not open browser automatically');
|
|
245
|
+
console.log('🌐 Please visit this URL to authorize:\n');
|
|
246
|
+
console.log(authUrl);
|
|
247
|
+
console.log();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log('⏳ Waiting for authorization...\n');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
setTimeout(
|
|
254
|
+
() => {
|
|
255
|
+
connections.forEach((socket) => socket.destroy());
|
|
256
|
+
connections.clear();
|
|
257
|
+
server.close();
|
|
258
|
+
reject(new Error('Authentication timeout (5 minutes)'));
|
|
259
|
+
},
|
|
260
|
+
5 * 60 * 1000,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Exchanges authorization code for short-lived access token
|
|
267
|
+
*/
|
|
268
|
+
private async exchangeCodeForToken(
|
|
269
|
+
code: string,
|
|
270
|
+
appId: string,
|
|
271
|
+
appSecret: string,
|
|
272
|
+
redirectUri: string,
|
|
273
|
+
): Promise<string> {
|
|
274
|
+
const params = new URLSearchParams({
|
|
275
|
+
client_id: appId,
|
|
276
|
+
client_secret: appSecret,
|
|
277
|
+
grant_type: 'authorization_code',
|
|
278
|
+
redirect_uri: redirectUri,
|
|
279
|
+
code: code,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const response = await fetch(
|
|
283
|
+
'https://api.instagram.com/oauth/access_token',
|
|
284
|
+
{
|
|
285
|
+
method: 'POST',
|
|
286
|
+
body: params,
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
const errorText = await response.text();
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Failed to exchange code for token: ${response.status} ${errorText}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const data = (await response.json()) as { access_token?: string };
|
|
298
|
+
|
|
299
|
+
if (!data.access_token) {
|
|
300
|
+
throw new Error('No access token in response');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return data.access_token;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Exchanges short-lived token for long-lived token (60 days)
|
|
308
|
+
*/
|
|
309
|
+
private async exchangeForLongLivedToken(
|
|
310
|
+
shortLivedToken: string,
|
|
311
|
+
appSecret: string,
|
|
312
|
+
): Promise<string> {
|
|
313
|
+
const params = new URLSearchParams({
|
|
314
|
+
grant_type: 'ig_exchange_token',
|
|
315
|
+
client_secret: appSecret,
|
|
316
|
+
access_token: shortLivedToken,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const response = await fetch(
|
|
320
|
+
`https://graph.instagram.com/access_token?${params.toString()}`,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
const errorText = await response.text();
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Failed to exchange for long-lived token: ${response.status} ${errorText}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const data = (await response.json()) as { access_token?: string };
|
|
331
|
+
|
|
332
|
+
if (!data.access_token) {
|
|
333
|
+
throw new Error('No long-lived access token in response');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return data.access_token;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Gets the Instagram user ID and username from the /me endpoint
|
|
341
|
+
*/
|
|
342
|
+
private async getInstagramUserId(
|
|
343
|
+
accessToken: string,
|
|
344
|
+
): Promise<{ id: string; username: string }> {
|
|
345
|
+
const params = new URLSearchParams({
|
|
346
|
+
fields: 'id,username',
|
|
347
|
+
access_token: accessToken,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const response = await fetch(
|
|
351
|
+
`https://graph.instagram.com/me?${params.toString()}`,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
const errorText = await response.text();
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Failed to get Instagram user info: ${response.status} ${errorText}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const data = (await response.json()) as { id?: string; username?: string };
|
|
362
|
+
|
|
363
|
+
if (!data.id || !data.username) {
|
|
364
|
+
throw new Error('No user ID or username in response');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { id: data.id, username: data.username };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
getSetupInstructions(): string {
|
|
371
|
+
return `
|
|
372
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
373
|
+
Instagram Authentication Setup
|
|
374
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
375
|
+
|
|
376
|
+
Interactive OAuth flow with automatic token exchange.
|
|
377
|
+
|
|
378
|
+
⚠️ PREREQUISITES
|
|
379
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
380
|
+
✅ Instagram Business or Creator account (NOT personal)
|
|
381
|
+
✅ Facebook account (for creating the app)
|
|
382
|
+
✅ ngrok or Cloudflare Tunnel (Meta doesn't allow localhost)
|
|
383
|
+
|
|
384
|
+
Convert to Business/Creator if needed:
|
|
385
|
+
Instagram app → Profile → Menu → Settings → Account
|
|
386
|
+
→ "Switch to Professional Account" → Choose Business or Creator
|
|
387
|
+
|
|
388
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
389
|
+
STEP 1: Create Facebook App with Instagram Use Case
|
|
390
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
391
|
+
1. Go to: https://developers.facebook.com
|
|
392
|
+
2. Click "Get Started" → Log in → Complete registration
|
|
393
|
+
3. Click "My Apps" → "Create App"
|
|
394
|
+
4. When asked about use case, select:
|
|
395
|
+
⭐ "Manage messaging & content on Instagram"
|
|
396
|
+
5. Select app type: "Business"
|
|
397
|
+
6. Fill in:
|
|
398
|
+
• App name: "My Instagram Uploader"
|
|
399
|
+
• Contact email: your.email@example.com
|
|
400
|
+
7. Click "Create App"
|
|
401
|
+
|
|
402
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
403
|
+
STEP 2: Publish App to Production (IMPORTANT!)
|
|
404
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
405
|
+
Publishing to production avoids test environment limitations.
|
|
406
|
+
|
|
407
|
+
1. In app dashboard, look for "App Mode" toggle or similar
|
|
408
|
+
2. Switch from "Development" to "Live" mode
|
|
409
|
+
3. Or find "Publish" button and click it
|
|
410
|
+
|
|
411
|
+
Note: For personal use, you don't need Meta verification.
|
|
412
|
+
|
|
413
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
414
|
+
STEP 3: Navigate to Instagram Customize Wizard
|
|
415
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
416
|
+
1. Go to: Dashboard → Use Cases
|
|
417
|
+
2. Find "Manage messaging & content on Instagram"
|
|
418
|
+
3. Click "Customize" button
|
|
419
|
+
|
|
420
|
+
You'll see a wizard with several steps. Follow them in order:
|
|
421
|
+
|
|
422
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
423
|
+
STEP 4: Copy App Credentials
|
|
424
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
425
|
+
At the top of the Customize page, you'll see:
|
|
426
|
+
• Instagram App ID (copy this!)
|
|
427
|
+
• Instagram App Secret (click "Show" to reveal, copy this!)
|
|
428
|
+
|
|
429
|
+
Keep these handy - you'll need them for the auth wizard.
|
|
430
|
+
|
|
431
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
432
|
+
STEP 5: Add Required Permissions
|
|
433
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
434
|
+
In the wizard, find "Add required messaging permissions" section:
|
|
435
|
+
|
|
436
|
+
1. Look for permissions list
|
|
437
|
+
2. Enable these permissions:
|
|
438
|
+
• instagram_business_basic
|
|
439
|
+
• instagram_business_content_publish
|
|
440
|
+
3. Save changes
|
|
441
|
+
|
|
442
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
443
|
+
STEP 6: Generate Access Token & Add Account
|
|
444
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
445
|
+
In the wizard, find "Generate access token" section:
|
|
446
|
+
|
|
447
|
+
1. Click "Add account"
|
|
448
|
+
2. You'll be prompted to authenticate with Instagram
|
|
449
|
+
3. If you have a personal account, convert it to Business/Creator
|
|
450
|
+
4. Allow access for the app
|
|
451
|
+
5. Complete the authorization flow
|
|
452
|
+
|
|
453
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
454
|
+
STEP 7: Set Up Tunnel (ngrok or Cloudflare)
|
|
455
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
456
|
+
⚠️ IMPORTANT: Meta doesn't allow localhost:3000 as callback domain!
|
|
457
|
+
You MUST use ngrok or Cloudflare Tunnel BEFORE running auth.
|
|
458
|
+
|
|
459
|
+
Option A - Using ngrok (simpler but unstable domain):
|
|
460
|
+
1. Install ngrok: https://ngrok.com/download
|
|
461
|
+
2. Run: ngrok http 3000
|
|
462
|
+
3. Copy the HTTPS URL (e.g., https://abc123.ngrok-free.app)
|
|
463
|
+
4. Keep ngrok running!
|
|
464
|
+
|
|
465
|
+
⚠️ WARNING: ngrok URLs change on restart!
|
|
466
|
+
You'll need to update Meta redirect URI each time.
|
|
467
|
+
|
|
468
|
+
Option B - Using Cloudflare Tunnel (stable domain, recommended):
|
|
469
|
+
1. Set up Cloudflare Tunnel: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/
|
|
470
|
+
2. Tunnel localhost:3000 to a stable domain
|
|
471
|
+
3. Your URL will be stable (e.g., https://your-domain.com)
|
|
472
|
+
|
|
473
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
474
|
+
STEP 8: Configure OAuth Redirect URI in Meta
|
|
475
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
476
|
+
In the wizard, find "Set up Instagram business login" → "Business login settings":
|
|
477
|
+
|
|
478
|
+
1. Add to "OAuth Redirect URIs":
|
|
479
|
+
https://your-tunnel-url/oauth2callback
|
|
480
|
+
|
|
481
|
+
Examples:
|
|
482
|
+
• ngrok: https://abc123.ngrok-free.app/oauth2callback
|
|
483
|
+
• Cloudflare: https://your-domain.com/oauth2callback
|
|
484
|
+
|
|
485
|
+
2. Click "Save"
|
|
486
|
+
|
|
487
|
+
⚠️ Make sure path ends with /oauth2callback (no trailing slash!)
|
|
488
|
+
|
|
489
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
490
|
+
STEP 9: Run Authentication Command
|
|
491
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
492
|
+
Make sure your tunnel (ngrok/Cloudflare) is running, then:
|
|
493
|
+
|
|
494
|
+
WITHOUT ngrok/Cloudflare (will likely fail with Instagram):
|
|
495
|
+
staticstripes auth --upload-name YOUR_UPLOAD_NAME
|
|
496
|
+
|
|
497
|
+
WITH ngrok/Cloudflare (recommended):
|
|
498
|
+
staticstripes auth --upload-name YOUR_UPLOAD_NAME \\
|
|
499
|
+
--oauth-redirect-url https://your-tunnel-url/oauth2callback
|
|
500
|
+
|
|
501
|
+
Example with ngrok:
|
|
502
|
+
staticstripes auth --upload-name ig_primary \\
|
|
503
|
+
--oauth-redirect-url https://abc123.ngrok-free.app/oauth2callback
|
|
504
|
+
|
|
505
|
+
Example with Cloudflare:
|
|
506
|
+
staticstripes auth --upload-name ig_primary \\
|
|
507
|
+
--oauth-redirect-url https://your-domain.com/oauth2callback
|
|
508
|
+
|
|
509
|
+
The command will:
|
|
510
|
+
1. Ask you to enter Instagram App ID
|
|
511
|
+
2. Ask you to enter Instagram App Secret
|
|
512
|
+
3. Use the redirect URL you specified (or default to localhost)
|
|
513
|
+
4. Start local server on port 3000
|
|
514
|
+
5. Open browser for Instagram authorization
|
|
515
|
+
6. Automatically exchange tokens (short-lived → long-lived)
|
|
516
|
+
7. Fetch your Instagram User ID
|
|
517
|
+
8. Save ALL credentials to .auth/YOUR_UPLOAD_NAME.json
|
|
518
|
+
|
|
519
|
+
Done! Your credentials are saved locally.
|
|
520
|
+
|
|
521
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
522
|
+
TOKEN REFRESH (Every 60 Days)
|
|
523
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
524
|
+
Tokens expire after 60 days. To refresh:
|
|
525
|
+
|
|
526
|
+
curl -X GET "https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=YOUR_CURRENT_TOKEN"
|
|
527
|
+
|
|
528
|
+
💡 Set a calendar reminder for 50 days!
|
|
529
|
+
|
|
530
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
531
|
+
TROUBLESHOOTING
|
|
532
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
533
|
+
❌ "redirect_uri_mismatch"
|
|
534
|
+
→ Meta doesn't accept localhost - use ngrok/Cloudflare
|
|
535
|
+
→ Make sure redirect URI in Meta matches redirectUri in code
|
|
536
|
+
→ Check for typos (no trailing slash!)
|
|
537
|
+
→ If using ngrok, domain changes on restart - update everywhere!
|
|
538
|
+
|
|
539
|
+
❌ "Can't find the wizard or Customize button"
|
|
540
|
+
→ Dashboard → Use Cases → "Manage messaging & content on Instagram"
|
|
541
|
+
→ If you don't see "Customize", your app might not have the right use case
|
|
542
|
+
|
|
543
|
+
❌ "Insufficient permissions" error
|
|
544
|
+
→ Make sure you completed Step 5 (Add required permissions)
|
|
545
|
+
→ Enable: instagram_business_basic, instagram_business_content_publish
|
|
546
|
+
|
|
547
|
+
❌ "Invalid access token"
|
|
548
|
+
→ Token might be expired (60 days max)
|
|
549
|
+
→ Re-run: staticstripes auth --upload-name YOUR_UPLOAD_NAME
|
|
550
|
+
|
|
551
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
552
|
+
REFERENCE LINKS
|
|
553
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
554
|
+
• Facebook Apps Dashboard:
|
|
555
|
+
https://developers.facebook.com/apps/
|
|
556
|
+
|
|
557
|
+
• Instagram Graph API docs:
|
|
558
|
+
https://developers.facebook.com/docs/instagram-api/
|
|
559
|
+
|
|
560
|
+
• ngrok download:
|
|
561
|
+
https://ngrok.com/download
|
|
562
|
+
|
|
563
|
+
• Cloudflare Tunnel:
|
|
564
|
+
https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/
|
|
565
|
+
|
|
566
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
}
|