@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.
- package/aimanager/InstagramOAuth.js +67 -4
- package/index.js +42 -7
- package/package.json +1 -1
|
@@ -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'))
|
|
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("(
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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;
|