@resultcrafter/aimanager-instagram-connector 0.2.0 → 0.2.1

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.
@@ -15,6 +15,58 @@ function getConfig() {
15
15
  };
16
16
  }
17
17
 
18
+ function getEncryptionKey() {
19
+ return process.env.TOKEN_ENCRYPTION_KEY || process.env.OAUTH_STATE_SECRET || null;
20
+ }
21
+
22
+ function getCipherKey() {
23
+ var key = getEncryptionKey();
24
+ if (!key) return null;
25
+ return crypto.createHash('sha256').update(key).digest();
26
+ }
27
+
28
+ function encrypt(data) {
29
+ var cipherKey = getCipherKey();
30
+ if (!cipherKey) return null;
31
+ var text = JSON.stringify(data);
32
+ var iv = crypto.randomBytes(16);
33
+ var cipher = crypto.createCipheriv('aes-256-gcm', cipherKey, iv);
34
+ var encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
35
+ var authTag = cipher.getAuthTag();
36
+ return {
37
+ encrypted: encrypted.toString('base64'),
38
+ iv: iv.toString('base64'),
39
+ authTag: authTag.toString('base64')
40
+ };
41
+ }
42
+
43
+ function decrypt(encryptedData) {
44
+ var cipherKey = getCipherKey();
45
+ if (!cipherKey) return null;
46
+ var decipher = crypto.createDecipheriv('aes-256-gcm', cipherKey, Buffer.from(encryptedData.iv, 'base64'));
47
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'));
48
+ var decrypted = Buffer.concat([
49
+ decipher.update(Buffer.from(encryptedData.encrypted, 'base64')),
50
+ decipher.final()
51
+ ]);
52
+ return JSON.parse(decrypted.toString('utf8'));
53
+ }
54
+
55
+ function encryptToken(tokenData) {
56
+ var encrypted = encrypt(tokenData);
57
+ if (encrypted) return encrypted;
58
+ return tokenData;
59
+ }
60
+
61
+ function decryptToken(stored) {
62
+ if (!stored) return null;
63
+ if (stored.encrypted && stored.iv && stored.authTag) {
64
+ var decrypted = decrypt(stored);
65
+ if (decrypted) return decrypted;
66
+ }
67
+ return stored;
68
+ }
69
+
18
70
  function generateState(sessionId, projectId) {
19
71
  var data = {
20
72
  sessionId: sessionId || crypto.randomBytes(16).toString('hex'),
@@ -38,7 +90,9 @@ function verifyState(state) {
38
90
  var decoded = JSON.parse(Buffer.from(state, 'base64url').toString());
39
91
  var hmac = crypto.createHmac('sha256', cfg.stateSecret);
40
92
  hmac.update(JSON.stringify(decoded.data));
41
- if (decoded.signature !== hmac.digest('hex')) return null;
93
+ if (decoded.signature !== hmac.digest('hex')) {
94
+ return null;
95
+ }
42
96
  if (Date.now() - decoded.data.timestamp > 600000) return null;
43
97
  return decoded.data;
44
98
  } catch (e) {
@@ -94,7 +148,7 @@ async function exchangeCodeForToken(code) {
94
148
  };
95
149
  }
96
150
 
97
- async function refreshToken(currentToken) {
151
+ async function refreshToken(currentToken, existingTokenData) {
98
152
  var cfg = getConfig();
99
153
  var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/refresh_access_token', {
100
154
  params: { grant_type: 'ig_refresh_token', access_token: currentToken }
@@ -103,6 +157,10 @@ async function refreshToken(currentToken) {
103
157
  access_token: res.data.access_token,
104
158
  token_type: res.data.token_type,
105
159
  expires_in: res.data.expires_in,
160
+ user_id: existingTokenData ? existingTokenData.user_id : undefined,
161
+ instagram_business_id: existingTokenData ? existingTokenData.instagram_business_id : undefined,
162
+ username: existingTokenData ? existingTokenData.username : undefined,
163
+ user_info: existingTokenData ? existingTokenData.user_info : undefined,
106
164
  created_at: Date.now(),
107
165
  expires_at: Date.now() + (res.data.expires_in * 1000)
108
166
  };
@@ -134,7 +192,10 @@ async function getTokenStatus(db, projectId) {
134
192
  if (!settings || !settings.ig_oauth_token) {
135
193
  return { connected: false, instagram_username: null };
136
194
  }
137
- var token = settings.ig_oauth_token;
195
+ var token = decryptToken(settings.ig_oauth_token);
196
+ if (!token) {
197
+ return { connected: false, instagram_username: null };
198
+ }
138
199
  var daysRemaining = token.expires_at ? Math.floor((token.expires_at - Date.now()) / (1000 * 60 * 60 * 24)) : null;
139
200
  return {
140
201
  connected: true,
@@ -151,5 +212,7 @@ module.exports = {
151
212
  refreshToken: refreshToken,
152
213
  getUserInfo: getUserInfo,
153
214
  verifyState: verifyState,
154
- getTokenStatus: getTokenStatus
215
+ getTokenStatus: getTokenStatus,
216
+ encryptToken: encryptToken,
217
+ decryptToken: decryptToken
155
218
  };
package/index.js CHANGED
@@ -5,6 +5,7 @@ const bodyParser = require("body-parser");
5
5
  const handlebars = require('handlebars');
6
6
  const path = require('path');
7
7
  const fs = require('fs');
8
+ const crypto = require('crypto');
8
9
  const pjson = require('./package.json');
9
10
  const winston = require('./winston');
10
11
  const url = require('url');
@@ -512,12 +513,45 @@ router.post('/tiledesk', async (req, res) => {
512
513
  return res.status(200).send({ message: "sent" });
513
514
  })
514
515
 
516
+ function verifyWebhookSignature(req) {
517
+ var signature = req.headers['x-hub-signature-256'];
518
+ if (!signature) {
519
+ winston.error("(ig) Missing x-hub-signature-256 header");
520
+ return false;
521
+ }
522
+ if (!APP_SECRET) {
523
+ winston.warn("(ig) APP_SECRET not configured — skipping signature validation");
524
+ return true;
525
+ }
526
+ var algo = 'sha256';
527
+ var expected = signature.replace('sha256=', '');
528
+ var hmac = crypto.createHmac(algo, APP_SECRET);
529
+ hmac.update(JSON.stringify(req.body));
530
+ var computed = hmac.digest('hex');
531
+ var valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(computed));
532
+ if (!valid) {
533
+ winston.error("(ig) Webhook signature mismatch — expected=" + expected + " computed=" + computed);
534
+ if (process.env.DEBUG_SIGNATURES === 'true') {
535
+ winston.debug("(ig) Raw body for signature: " + JSON.stringify(req.body));
536
+ }
537
+ }
538
+ return valid;
539
+ }
540
+
515
541
  router.post('/webhookFB', async (req, res) => {
516
542
 
517
- winston.verbose("(fbm) Message received from Facebook Instagram");
543
+ winston.verbose("(ig) Webhook received");
544
+
545
+ if (!verifyWebhookSignature(req)) {
546
+ return res.sendStatus(403);
547
+ }
548
+
549
+ res.status(200).send('EVENT_RECEIVED');
518
550
 
519
- let body = req.body;
520
- if (body.object === 'page') {
551
+ setImmediate(async () => {
552
+ try {
553
+ let body = req.body;
554
+ winston.verbose("(ig) Processing webhook asynchronously");
521
555
 
522
556
  let page_id = body.entry[0].id;
523
557
  let PAGE_KEY = "instagram-page-" + page_id;
@@ -606,9 +640,10 @@ router.post('/webhookFB', async (req, res) => {
606
640
  }
607
641
 
608
642
  })
609
- }
610
-
611
- return res.status(200).send('EVENT_RECEIVED');
643
+ } catch (err) {
644
+ winston.error("(ig) Async webhook processing error: ", err?.response?.data || err.message || err);
645
+ }
646
+ });
612
647
  })
613
648
 
614
649
  router.get('/webhookFB', async (req, res) => {
@@ -688,7 +723,7 @@ router.get('/oauth-callback', async (req, res) => {
688
723
 
689
724
  var CONTENT_KEY = "instagram-" + projectId;
690
725
  var settings = await db.get(CONTENT_KEY) || {};
691
- settings.ig_oauth_token = tokenData;
726
+ settings.ig_oauth_token = instagramOAuth.encryptToken(tokenData);
692
727
  settings.instagram_username = tokenData.username;
693
728
  settings.instagram_business_id = tokenData.instagram_business_id;
694
729
  settings.connected = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resultcrafter/aimanager-instagram-connector",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI Manager Instagram DM connector",
5
5
  "main": "index.js",
6
6
  "scripts": {