@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.
Files changed (128) hide show
  1. package/Makefile +37 -4
  2. package/dist/asset-manager.d.ts +1 -0
  3. package/dist/asset-manager.d.ts.map +1 -1
  4. package/dist/asset-manager.js +3 -0
  5. package/dist/asset-manager.js.map +1 -1
  6. package/dist/cli/ai-generation-strategy-factory.d.ts +23 -0
  7. package/dist/cli/ai-generation-strategy-factory.d.ts.map +1 -0
  8. package/dist/cli/ai-generation-strategy-factory.js +44 -0
  9. package/dist/cli/ai-generation-strategy-factory.js.map +1 -0
  10. package/dist/cli/ai-generation-strategy.d.ts +33 -0
  11. package/dist/cli/ai-generation-strategy.d.ts.map +1 -0
  12. package/dist/cli/ai-generation-strategy.js +3 -0
  13. package/dist/cli/ai-generation-strategy.js.map +1 -0
  14. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +38 -0
  15. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -0
  16. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +174 -0
  17. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -0
  18. package/dist/cli/auth-strategy-factory.d.ts +31 -0
  19. package/dist/cli/auth-strategy-factory.d.ts.map +1 -0
  20. package/dist/cli/auth-strategy-factory.js +61 -0
  21. package/dist/cli/auth-strategy-factory.js.map +1 -0
  22. package/dist/cli/auth-strategy.d.ts +31 -0
  23. package/dist/cli/auth-strategy.d.ts.map +1 -0
  24. package/dist/cli/auth-strategy.js +3 -0
  25. package/dist/cli/auth-strategy.js.map +1 -0
  26. package/dist/cli/commands/add-assets.d.ts +3 -0
  27. package/dist/cli/commands/add-assets.d.ts.map +1 -0
  28. package/dist/cli/commands/add-assets.js +113 -0
  29. package/dist/cli/commands/add-assets.js.map +1 -0
  30. package/dist/cli/commands/auth.d.ts +6 -0
  31. package/dist/cli/commands/auth.d.ts.map +1 -0
  32. package/dist/cli/commands/auth.js +103 -0
  33. package/dist/cli/commands/auth.js.map +1 -0
  34. package/dist/cli/commands/bootstrap.d.ts +3 -0
  35. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  36. package/dist/cli/commands/bootstrap.js +49 -0
  37. package/dist/cli/commands/bootstrap.js.map +1 -0
  38. package/dist/cli/commands/generate.d.ts +3 -0
  39. package/dist/cli/commands/generate.d.ts.map +1 -0
  40. package/dist/cli/commands/generate.js +199 -0
  41. package/dist/cli/commands/generate.js.map +1 -0
  42. package/dist/cli/commands/upload.d.ts +6 -0
  43. package/dist/cli/commands/upload.d.ts.map +1 -0
  44. package/dist/cli/commands/upload.js +67 -0
  45. package/dist/cli/commands/upload.js.map +1 -0
  46. package/dist/cli/instagram/instagram-auth-strategy.d.ts +31 -0
  47. package/dist/cli/instagram/instagram-auth-strategy.d.ts.map +1 -0
  48. package/dist/cli/instagram/instagram-auth-strategy.js +505 -0
  49. package/dist/cli/instagram/instagram-auth-strategy.js.map +1 -0
  50. package/dist/cli/instagram/instagram-upload-strategy.d.ts +45 -0
  51. package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -0
  52. package/dist/cli/instagram/instagram-upload-strategy.js +303 -0
  53. package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -0
  54. package/dist/cli/s3/s3-upload-strategy.d.ts +18 -0
  55. package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -0
  56. package/dist/cli/s3/s3-upload-strategy.js +153 -0
  57. package/dist/cli/s3/s3-upload-strategy.js.map +1 -0
  58. package/dist/cli/upload-strategy-factory.d.ts +23 -0
  59. package/dist/cli/upload-strategy-factory.d.ts.map +1 -0
  60. package/dist/cli/upload-strategy-factory.js +49 -0
  61. package/dist/cli/upload-strategy-factory.js.map +1 -0
  62. package/dist/cli/upload-strategy.d.ts +25 -0
  63. package/dist/cli/upload-strategy.d.ts.map +1 -0
  64. package/dist/cli/upload-strategy.js +3 -0
  65. package/dist/cli/upload-strategy.js.map +1 -0
  66. package/dist/cli/youtube/youtube-auth-strategy.d.ts +11 -0
  67. package/dist/cli/youtube/youtube-auth-strategy.d.ts.map +1 -0
  68. package/dist/cli/youtube/youtube-auth-strategy.js +320 -0
  69. package/dist/cli/youtube/youtube-auth-strategy.js.map +1 -0
  70. package/dist/cli/youtube/youtube-upload-strategy.d.ts +22 -0
  71. package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -0
  72. package/dist/cli/youtube/youtube-upload-strategy.js +117 -0
  73. package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -0
  74. package/dist/cli.js +11 -281
  75. package/dist/cli.js.map +1 -1
  76. package/dist/html-parser.d.ts +3 -4
  77. package/dist/html-parser.d.ts.map +1 -1
  78. package/dist/html-parser.js +20 -17
  79. package/dist/html-parser.js.map +1 -1
  80. package/dist/html-project-parser.d.ts +64 -1
  81. package/dist/html-project-parser.d.ts.map +1 -1
  82. package/dist/html-project-parser.js +695 -57
  83. package/dist/html-project-parser.js.map +1 -1
  84. package/dist/lib/file.d.ts +2 -0
  85. package/dist/lib/file.d.ts.map +1 -0
  86. package/dist/lib/file.js +13 -0
  87. package/dist/lib/file.js.map +1 -0
  88. package/dist/lib/net.d.ts +19 -0
  89. package/dist/lib/net.d.ts.map +1 -0
  90. package/dist/lib/net.js +101 -0
  91. package/dist/lib/net.js.map +1 -0
  92. package/dist/project.d.ts +18 -2
  93. package/dist/project.d.ts.map +1 -1
  94. package/dist/project.js +65 -1
  95. package/dist/project.js.map +1 -1
  96. package/dist/type.d.ts +43 -4
  97. package/dist/type.d.ts.map +1 -1
  98. package/dist/youtube-uploader.d.ts +40 -0
  99. package/dist/youtube-uploader.d.ts.map +1 -0
  100. package/dist/youtube-uploader.js +227 -0
  101. package/dist/youtube-uploader.js.map +1 -0
  102. package/package.json +6 -2
  103. package/src/asset-manager.ts +4 -0
  104. package/src/cli/ai-generation-strategy-factory.ts +48 -0
  105. package/src/cli/ai-generation-strategy.ts +35 -0
  106. package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +266 -0
  107. package/src/cli/auth-strategy-factory.ts +67 -0
  108. package/src/cli/auth-strategy.ts +37 -0
  109. package/src/cli/commands/add-assets.ts +159 -0
  110. package/src/cli/commands/auth.ts +120 -0
  111. package/src/cli/commands/bootstrap.ts +57 -0
  112. package/src/cli/commands/generate.ts +242 -0
  113. package/src/cli/commands/upload.ts +83 -0
  114. package/src/cli/instagram/instagram-auth-strategy.ts +569 -0
  115. package/src/cli/instagram/instagram-upload-strategy.ts +398 -0
  116. package/src/cli/s3/s3-upload-strategy.ts +198 -0
  117. package/src/cli/upload-strategy-factory.ts +55 -0
  118. package/src/cli/upload-strategy.ts +31 -0
  119. package/src/cli/youtube/youtube-auth-strategy.ts +323 -0
  120. package/src/cli/youtube/youtube-upload-strategy.ts +174 -0
  121. package/src/cli.ts +13 -391
  122. package/src/html-parser.ts +23 -21
  123. package/src/html-project-parser.ts +821 -62
  124. package/src/lib/file.ts +11 -0
  125. package/src/lib/net.ts +120 -0
  126. package/src/project.ts +81 -1
  127. package/src/type.ts +49 -4
  128. 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
+ }