@goniglep57/node 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/alerts.js +103 -0
- package/dropbox.js +274 -0
- package/index.js +7 -0
- package/ncrypt.js +67 -0
- package/package.json +16 -0
- package/readme.md +12 -0
- package/secret.js +63 -0
- package/server.js +78 -0
- package/tests/index.test.js +67 -0
- package/tests/rate-limit-test.js +61 -0
- package/util.js +11 -0
package/alerts.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const nodemailer = require('nodemailer')
|
|
2
|
+
|
|
3
|
+
let transporter = null
|
|
4
|
+
let config = null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configure the alerts module with Gmail credentials
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.gmailUser - Gmail address to send from
|
|
10
|
+
* @param {string} options.gmailAppPassword - Gmail App Password (not regular password)
|
|
11
|
+
* @param {string} options.to - Default recipient email address
|
|
12
|
+
* @param {string} [options.defaultSubject='Alert'] - Default email subject
|
|
13
|
+
*/
|
|
14
|
+
const configure = (options) => {
|
|
15
|
+
const { gmailUser, gmailAppPassword, to, defaultSubject = 'Alert' } = options
|
|
16
|
+
|
|
17
|
+
if (!gmailUser || !gmailAppPassword || !to) {
|
|
18
|
+
throw new Error('alerts.configure requires gmailUser, gmailAppPassword, and to')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
config = {
|
|
22
|
+
from: gmailUser,
|
|
23
|
+
to,
|
|
24
|
+
defaultSubject
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
transporter = nodemailer.createTransport({
|
|
28
|
+
service: 'gmail',
|
|
29
|
+
auth: {
|
|
30
|
+
user: gmailUser,
|
|
31
|
+
pass: gmailAppPassword
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return { configured: true }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Send an alert email
|
|
40
|
+
* @param {string|Object} messageOrOptions - Message string or options object
|
|
41
|
+
* @param {string} [messageOrOptions.message] - Email body content
|
|
42
|
+
* @param {string} [messageOrOptions.subject] - Email subject (overrides default)
|
|
43
|
+
* @param {string} [messageOrOptions.to] - Recipient email (overrides default)
|
|
44
|
+
* @param {boolean} [messageOrOptions.html=false] - Whether message is HTML
|
|
45
|
+
* @returns {Promise<Object>} - Nodemailer send result
|
|
46
|
+
*/
|
|
47
|
+
const send = async (messageOrOptions) => {
|
|
48
|
+
if (!transporter || !config) {
|
|
49
|
+
throw new Error('alerts not configured. Call alerts.configure() first')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let message, subject, to, html
|
|
53
|
+
|
|
54
|
+
if (typeof messageOrOptions === 'string') {
|
|
55
|
+
message = messageOrOptions
|
|
56
|
+
subject = config.defaultSubject
|
|
57
|
+
to = config.to
|
|
58
|
+
html = false
|
|
59
|
+
} else {
|
|
60
|
+
message = messageOrOptions.message
|
|
61
|
+
subject = messageOrOptions.subject || config.defaultSubject
|
|
62
|
+
to = messageOrOptions.to || config.to
|
|
63
|
+
html = messageOrOptions.html || false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!message) {
|
|
67
|
+
throw new Error('Message is required')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mailOptions = {
|
|
71
|
+
from: config.from,
|
|
72
|
+
to,
|
|
73
|
+
subject,
|
|
74
|
+
[html ? 'html' : 'text']: message
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await transporter.sendMail(mailOptions)
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if alerts module is configured
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
const isConfigured = () => {
|
|
86
|
+
return transporter !== null && config !== null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reset configuration (useful for testing)
|
|
91
|
+
*/
|
|
92
|
+
const reset = () => {
|
|
93
|
+
transporter = null
|
|
94
|
+
config = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
configure,
|
|
99
|
+
send,
|
|
100
|
+
isConfigured,
|
|
101
|
+
reset
|
|
102
|
+
}
|
|
103
|
+
|
package/dropbox.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const { objectToUriParams } = require('./util.js')
|
|
2
|
+
|
|
3
|
+
const DROPBOX_AUTH_TOKEN_URI = process.env.DROPBOX_AUTH_TOKEN_URI
|
|
4
|
+
const DROPBOX_AUTH_SHARED_SECRET = process.env.DROPBOX_AUTH_SHARED_SECRET
|
|
5
|
+
const DROPBOX_API_URI = process.env.DROPBOX_API_URI || 'https://api.dropboxapi.com/2'
|
|
6
|
+
|
|
7
|
+
const Dropbox = (httpClient, config={retry:false}) => {
|
|
8
|
+
const data = {}
|
|
9
|
+
|
|
10
|
+
const getHeaders = (teamCall=false) => {
|
|
11
|
+
const headers = { 'Authorization': `Bearer ${data.access_token}`, 'Content-Type': 'application/json' }
|
|
12
|
+
if (data.dropboxUser && !teamCall) {
|
|
13
|
+
headers['Dropbox-API-Select-User'] = data.dropboxUser
|
|
14
|
+
if(data.namespace){
|
|
15
|
+
headers['Dropbox-API-Path-Root'] = `{".tag": "namespace_id", "namespace_id": "${data.namespace}"}`
|
|
16
|
+
} else {
|
|
17
|
+
headers['Dropbox-API-Path-Root'] = `{".tag": "home"}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { headers }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const setUser = (userId) => {
|
|
25
|
+
data.dropboxUser = userId
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const setNamespace = (namespaceId) => {
|
|
29
|
+
data.namespace = namespaceId
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const setToken = (token) => {
|
|
33
|
+
data.access_token = token
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const manualRefreshToken = async (refresh_token, client_id, client_secret) => {
|
|
37
|
+
const creds = Buffer.from(`${client_id}:${client_secret}`).toString('base64')
|
|
38
|
+
const response = await httpClient.post(
|
|
39
|
+
'https://api.dropboxapi.com/oauth2/token',
|
|
40
|
+
new URLSearchParams({
|
|
41
|
+
grant_type: 'refresh_token',
|
|
42
|
+
refresh_token: refresh_token
|
|
43
|
+
}).toString(),
|
|
44
|
+
{
|
|
45
|
+
headers: {
|
|
46
|
+
'Authorization': `Basic ${creds}`,
|
|
47
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
data.access_token = response.data.access_token
|
|
53
|
+
|
|
54
|
+
return response.data
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getAccessToken = async (dropboxAccountId) => {
|
|
58
|
+
const response = await httpClient.get(`${DROPBOX_AUTH_TOKEN_URI}${dropboxAccountId}`, {
|
|
59
|
+
headers: {
|
|
60
|
+
'x-shared-secret': DROPBOX_AUTH_SHARED_SECRET
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
data.access_token = response.data.access_token
|
|
64
|
+
data.dropbox_account_id = dropboxAccountId
|
|
65
|
+
return response.data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const getTemporaryLink = async (data) => {
|
|
69
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/files/get_temporary_link`, data, getHeaders())
|
|
70
|
+
return response.data
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const listFolder = async (body) => {
|
|
74
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/files/list_folder`, body, getHeaders())
|
|
75
|
+
return response.data
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const listFolderContinue = async (body) => {
|
|
79
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/files/list_folder/continue`, body, getHeaders())
|
|
80
|
+
return response.data
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const getMetadata = async (body) => {
|
|
84
|
+
try {
|
|
85
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/files/get_metadata`, body, getHeaders())
|
|
86
|
+
console.log(response.data)
|
|
87
|
+
return response.data
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Also handle if error is thrown as part of response exception (e.g. non-2xx)
|
|
90
|
+
if (err.response?.data?.error_summary === 'path/not_found/') {
|
|
91
|
+
console.error('Path not found in dropbox', body)
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
// throw err
|
|
95
|
+
console.error(err.message)
|
|
96
|
+
// throw err
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const getSharedLinkMetadata = async (body) => {
|
|
101
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/sharing/get_shared_link_metadata`, body, getHeaders())
|
|
102
|
+
return response.data
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const getLatestCursor = async (body) => {
|
|
106
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/files/list_folder/get_latest_cursor`, body, getHeaders())
|
|
107
|
+
return response.data
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const convertOAuth2Token = async (body) => {
|
|
111
|
+
//the body here is converted into a uri params string
|
|
112
|
+
// OAuth2 token endpoint is separate from API v2 endpoints
|
|
113
|
+
const uriParams = objectToUriParams(body)
|
|
114
|
+
const response = await httpClient.post(`https://api.dropbox.com/oauth2/token?${uriParams}`, null, {})
|
|
115
|
+
data.access_token = response.data.access_token
|
|
116
|
+
return response.data
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const teamGetInfo = async () => {
|
|
120
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/team/get_info`, null, getHeaders(true))
|
|
121
|
+
return response.data
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const teamMemebersList = async () => {
|
|
125
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/team/members/list`, null, getHeaders(true))
|
|
126
|
+
return response.data
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const teamNamespacesList = async () => {
|
|
130
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/team/namespaces/list`, null, getHeaders(true))
|
|
131
|
+
return response.data
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const teamNamespacesListContinue = async (body) => {
|
|
135
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/team/namespaces/list/continue`, body, getHeaders(true))
|
|
136
|
+
return response.data
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const retryOnExpiredToken = (cb) => {
|
|
140
|
+
return async(...args) => {
|
|
141
|
+
try {
|
|
142
|
+
return await cb(...args)
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// Handle Dropbox expired auth token error
|
|
145
|
+
if (err.response?.data?.error_summary?.includes('expired_access_token')) {
|
|
146
|
+
console.error('Dropbox auth token expired:', err.message)
|
|
147
|
+
await getAccessToken(data.dropbox_account_id)
|
|
148
|
+
return cb(...args)
|
|
149
|
+
}
|
|
150
|
+
console.error(err.message)
|
|
151
|
+
throw err
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const retryOnServerError = (cb, maxRetries = 3, delayMs = 1000) => {
|
|
157
|
+
return async (...args) => {
|
|
158
|
+
let lastError
|
|
159
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
160
|
+
try {
|
|
161
|
+
return await cb(...args)
|
|
162
|
+
} catch (err) {
|
|
163
|
+
lastError = err
|
|
164
|
+
// Only retry on 500 server errors
|
|
165
|
+
if (err.response?.status === 500 && attempt < maxRetries) {
|
|
166
|
+
console.error(`Dropbox 500 error (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delayMs}ms...`)
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
throw err
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw lastError
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const retryOnRateLimit = (cb, maxRetries = 5, initialDelayMs = 1000) => {
|
|
178
|
+
return async (...args) => {
|
|
179
|
+
let lastError
|
|
180
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
181
|
+
try {
|
|
182
|
+
return await cb(...args)
|
|
183
|
+
} catch (err) {
|
|
184
|
+
lastError = err
|
|
185
|
+
// Retry on 429 rate limit with exponential backoff
|
|
186
|
+
if (err.response?.status === 429 && attempt < maxRetries) {
|
|
187
|
+
// Check if Dropbox sent a Retry-After header
|
|
188
|
+
const retryAfter = err.response?.headers?.['retry-after']
|
|
189
|
+
const delay = retryAfter
|
|
190
|
+
? parseInt(retryAfter, 10) * 1000
|
|
191
|
+
: initialDelayMs * Math.pow(2, attempt)
|
|
192
|
+
console.error(`Dropbox rate limit 429 (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...`)
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
throw err
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throw lastError
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const getCurrentAccount = async () => {
|
|
204
|
+
const response = await httpClient.post(`${DROPBOX_API_URI}/users/get_current_account`, null, getHeaders())
|
|
205
|
+
return response.data
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const createFolder = async (path) => {
|
|
209
|
+
const response = await httpClient.post(
|
|
210
|
+
`${DROPBOX_API_URI}/files/create_folder_v2`,
|
|
211
|
+
{ path, autorename: false },
|
|
212
|
+
getHeaders()
|
|
213
|
+
)
|
|
214
|
+
return response.data
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const createSharedLink = async (path) => {
|
|
218
|
+
try {
|
|
219
|
+
const response = await httpClient.post(
|
|
220
|
+
`${DROPBOX_API_URI}/sharing/create_shared_link_with_settings`,
|
|
221
|
+
{ path, settings: { requested_visibility: { '.tag': 'public' } } },
|
|
222
|
+
getHeaders()
|
|
223
|
+
)
|
|
224
|
+
return response.data.url
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// If link already exists, get the existing one
|
|
227
|
+
if (err.response?.data?.error_summary?.includes('shared_link_already_exists')) {
|
|
228
|
+
const listResponse = await httpClient.post(
|
|
229
|
+
`${DROPBOX_API_URI}/sharing/list_shared_links`,
|
|
230
|
+
{ path, direct_only: true },
|
|
231
|
+
getHeaders()
|
|
232
|
+
)
|
|
233
|
+
if (listResponse.data.links && listResponse.data.links.length > 0) {
|
|
234
|
+
return listResponse.data.links[0].url
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw err
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const exportFunctions = {
|
|
242
|
+
getTemporaryLink,
|
|
243
|
+
listFolder,
|
|
244
|
+
listFolderContinue,
|
|
245
|
+
getMetadata,
|
|
246
|
+
getSharedLinkMetadata,
|
|
247
|
+
getLatestCursor,
|
|
248
|
+
convertOAuth2Token,
|
|
249
|
+
teamGetInfo,
|
|
250
|
+
getCurrentAccount,
|
|
251
|
+
teamMemebersList,
|
|
252
|
+
teamNamespacesList,
|
|
253
|
+
createFolder,
|
|
254
|
+
createSharedLink
|
|
255
|
+
}
|
|
256
|
+
for (const key in exportFunctions) {
|
|
257
|
+
if(config.retry === true){
|
|
258
|
+
exportFunctions[key] = retryOnExpiredToken(exportFunctions[key])
|
|
259
|
+
}
|
|
260
|
+
exportFunctions[key] = retryOnRateLimit(exportFunctions[key])
|
|
261
|
+
exportFunctions[key] = retryOnServerError(exportFunctions[key])
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
exportFunctions.getAccessToken = getAccessToken
|
|
265
|
+
exportFunctions.convertOAuth2Token = convertOAuth2Token
|
|
266
|
+
exportFunctions.setUser = setUser
|
|
267
|
+
exportFunctions.setNamespace = setNamespace
|
|
268
|
+
exportFunctions.setToken = setToken
|
|
269
|
+
exportFunctions.manualRefreshToken = manualRefreshToken
|
|
270
|
+
|
|
271
|
+
return exportFunctions
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = Dropbox
|
package/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//generator:exportdir
|
|
2
|
+
exports.alerts = require('./alerts.js')
|
|
3
|
+
exports.dropbox = require('./dropbox.js')
|
|
4
|
+
exports.ncrypt = require('./ncrypt.js')
|
|
5
|
+
exports.secret = require('./secret.js')
|
|
6
|
+
exports.server = require('./server.js')
|
|
7
|
+
exports.util = require('./util.js')
|
package/ncrypt.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const crypto = require("crypto")
|
|
2
|
+
|
|
3
|
+
const pubKeyEncrypt = (toEncrypt, pubKey) => {
|
|
4
|
+
const buffer = Buffer.from(toEncrypt);
|
|
5
|
+
const encrypted = crypto.publicEncrypt(pubKey, buffer)
|
|
6
|
+
return encrypted.toString('base64')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const privateKeyDecrypt = (toDecrypt, privateKey) => {
|
|
10
|
+
const buffer = Buffer.from(toDecrypt, 'base64');
|
|
11
|
+
const decrypted = crypto.privateDecrypt(privateKey, buffer)
|
|
12
|
+
return decrypted.toString('utf-8')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const symKeyEncrypt = (toEncrypt, symKey) => {
|
|
16
|
+
const iv = crypto.randomBytes(16)
|
|
17
|
+
const cipher = crypto.createCipheriv('aes256', symKey, iv)
|
|
18
|
+
let cipheredData = cipher.update(toEncrypt, 'utf-8', 'hex') + cipher.final('hex')
|
|
19
|
+
const message = `${iv.toString('hex')}-${cipheredData}`
|
|
20
|
+
return message
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const symKeyDecrypt = (toDecrypt, symKey) => {
|
|
24
|
+
const [iv, ...data] = toDecrypt.split('-')
|
|
25
|
+
const decipher = crypto.createDecipheriv('aes256', symKey, Buffer.from(iv, 'hex'))
|
|
26
|
+
const deciphered = decipher.update(data.join('-'), 'hex', 'utf-8') + decipher.final('utf-8')
|
|
27
|
+
return deciphered
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const hybridEncrypt = (toEncrypt, pubKey) => {
|
|
31
|
+
const symKey = crypto.randomBytes(32)
|
|
32
|
+
const encryptedSymKey = pubKeyEncrypt(symKey.toString('hex'), pubKey)
|
|
33
|
+
const encryptedData = symKeyEncrypt(toEncrypt, symKey)
|
|
34
|
+
return `${encryptedSymKey}-${encryptedData}`
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const hybridDecrypt = (toDecrypt, privateKey) => {
|
|
39
|
+
const [encryptedSymKey, ...hybridData] = toDecrypt.split('-')
|
|
40
|
+
const symKey = Buffer.from(privateKeyDecrypt(encryptedSymKey, privateKey), 'hex')
|
|
41
|
+
const message = symKeyDecrypt(hybridData.join('-'), symKey, 'hex')
|
|
42
|
+
return message
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Hashing functions
|
|
46
|
+
const md5 = (data) => {
|
|
47
|
+
return crypto.createHash('md5').update(data, 'utf-8').digest('hex')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sha1 = (data) => {
|
|
51
|
+
return crypto.createHash('sha1').update(data, 'utf-8').digest('hex')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
pubKeyEncrypt,
|
|
56
|
+
privateKeyDecrypt,
|
|
57
|
+
symKeyEncrypt,
|
|
58
|
+
symKeyDecrypt,
|
|
59
|
+
hybridEncrypt,
|
|
60
|
+
hybridDecrypt,
|
|
61
|
+
encrypt: hybridEncrypt,
|
|
62
|
+
decrypt: hybridDecrypt,
|
|
63
|
+
hash: {
|
|
64
|
+
md5,
|
|
65
|
+
sha1
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goniglep57/node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A package full of utility functions",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"author": "",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@aws-sdk/client-s3": "^3.957.0",
|
|
13
|
+
"@aws-sdk/client-sqs": "^3.957.0",
|
|
14
|
+
"nodemailer": "^6.9.16"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/readme.md
ADDED
package/secret.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const getSecretName = () => {
|
|
2
|
+
if(true ||process.env.NODE_ENV !== 'production'){
|
|
3
|
+
return 'local'
|
|
4
|
+
}
|
|
5
|
+
return PRODUCTION_SECRET_NAME
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const getSecret = async (secretName) => {
|
|
9
|
+
if(true ||process.env.NODE_ENV !== 'production'){
|
|
10
|
+
return {
|
|
11
|
+
name:'local',
|
|
12
|
+
publicKey:`-----BEGIN PUBLIC KEY-----
|
|
13
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoKPg+zo58M1u/YRTbm2H
|
|
14
|
+
3zGzyUWLo5dSI/j9woqv6XfBZeVKeTJPqcy3IaciGB1euppmu4eEzHLFijHeoO23
|
|
15
|
+
C1KffrHTJV8wk6Tm3pXuYfdKm+2sowpmHmzD1boMPKAUckSKg7vFWZxg1Xis4Cco
|
|
16
|
+
eq1cGpNxeB1e8hGYv1Hf3sus1oTh4bHaiLmaSNLYWXws2VvzN4W4w5ezbz4hvSV8
|
|
17
|
+
LKCMPNyzOcOT6ffGQueIsUMAhVZqeNdriExNeC6J+7CCh4kYiFzbRqvMH+GCwtoK
|
|
18
|
+
i3qL4RYTaPdikQWmsxJ+Pza136b6PfG1KU/v8zF+YmwQRg6IpKs7Xo7/CHKEvQ9I
|
|
19
|
+
GwIDAQAB
|
|
20
|
+
-----END PUBLIC KEY-----`,
|
|
21
|
+
privateKey:`-----BEGIN PRIVATE KEY-----
|
|
22
|
+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCgo+D7OjnwzW79
|
|
23
|
+
hFNubYffMbPJRYujl1Ij+P3Ciq/pd8Fl5Up5Mk+pzLchpyIYHV66mma7h4TMcsWK
|
|
24
|
+
Md6g7bcLUp9+sdMlXzCTpObele5h90qb7ayjCmYebMPVugw8oBRyRIqDu8VZnGDV
|
|
25
|
+
eKzgJyh6rVwak3F4HV7yEZi/Ud/ey6zWhOHhsdqIuZpI0thZfCzZW/M3hbjDl7Nv
|
|
26
|
+
PiG9JXwsoIw83LM5w5Pp98ZC54ixQwCFVmp412uITE14Lon7sIKHiRiIXNtGq8wf
|
|
27
|
+
4YLC2gqLeovhFhNo92KRBaazEn4/NrXfpvo98bUpT+/zMX5ibBBGDoikqztejv8I
|
|
28
|
+
coS9D0gbAgMBAAECggEAA4FP6Wtt1DlHULTQdTpimpYlDCE+WMV2f15xews3p2p7
|
|
29
|
+
h9Om+loErvnOjSducQRDCuNPlovwc92VPdy0tOI6+j5FSKhsqQYYs0AH4Zd36h9R
|
|
30
|
+
O3HGMJCLheJtq34xwijkCVJBi3pPZhMpcb7+Vq3EDlk2B0ZsVWiep72GjbYlomDa
|
|
31
|
+
fL3nQrTTStBv7P61HMeLR0HZzvaOM/OvyyZswqgiOw3vbp8GYrm9YHDNcZhwADmH
|
|
32
|
+
y5kVvHuCOXQqicrDupKBLLT4HtDLj7KnRwabVg5gTbQNC26/1z2KNH/6rKNVELCb
|
|
33
|
+
c2jFNqEYg1YSgJe3yl8CVFhGqsvjDKMoAdooU0CEkQKBgQDOObvjqFlAr4WVQ+0r
|
|
34
|
+
/M9u3XL3hg7ucAuO0Kbkcw11IgBS+oHFClfZBSucaW9Jqowje9lVaenTsRoPmLgq
|
|
35
|
+
UbU4UygIhnBYA3HmZpwWxcK3QNCFO3icI0BAhuooxKb3JzhT61CbH44nTaPjf2Y4
|
|
36
|
+
oLiNwebzzU1vyaqbQrt2wTEWjwKBgQDHaYPj+lC5a7DYAeth+yt6h1gkuOrx+mos
|
|
37
|
+
tGRp2d3dp2sld58i3LDQWA4QW3XP6iekn1nBWpww6QdHMIEzYrCBIE2UhYT4nLK9
|
|
38
|
+
rjFOgrE4EPS5C1X80uUPxbA6JqGlKk/9syJYELpalKzLK24c7IXpv3sum3lkXyYn
|
|
39
|
+
cNRh3xLbtQKBgFv7Nkmpef23C/7yqCN5hE/vp+qtOZWyFgWiHP5CLttyyfA42Y64
|
|
40
|
+
OUyL2NLP2BpkXlMpV1uYfPm1wWj5dhMyFIFF3dJHnG41QyU/2RjzJhE0cI0sZHRH
|
|
41
|
+
6M2fH2wFom8UaDRN/crivTXclF/RIBfb39Uiik9t378flN/hWNnz+2ATAoGBAJos
|
|
42
|
+
/GoRKMSTfKElqsWQmmcbqYyK5Cu+fsSdnF0ysxUi0faf7AvaMWKEEiuXaxkbYxcm
|
|
43
|
+
lRCna8dtA9GIsPv3u5u6TAEe0lYdYRzCRyD6buZ4RZ2kJBFbRPrSz5PGNogqlLoD
|
|
44
|
+
+/O2yz4pYQecRoBaZpb5EtZQIqT149vuomYhQMVZAoGBAI/XzOvY28QeFOES9oMl
|
|
45
|
+
vrqTC28Rn6qOeZ/JKoDI1u33JaldiuU8PzF2kTxwAweIojF96lLB5o1fjmKi0Tku
|
|
46
|
+
4qt8Snv20TIj4+Y9MuH+ullgR6kMqviH+pZ1uxQYUax5NdIWBN6c1SrV1EhBmtzZ
|
|
47
|
+
aAfsaNXTVik3/tuvd8ushVWC
|
|
48
|
+
-----END PRIVATE KEY-----`
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: PRODUCTION_SECRET_NAME,
|
|
54
|
+
publicKey: PRODUCTION_PUBLIC_KEY,
|
|
55
|
+
privateKey: PRODUCTION_PRIVATE_KEY
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
getSecretName,
|
|
61
|
+
getSecret
|
|
62
|
+
}
|
|
63
|
+
|
package/server.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const addDbOnApiRoute = (app, pool, {baseRoute='/api'}) => {
|
|
2
|
+
app.use(baseRoute, async (req, res, next)=>{
|
|
3
|
+
try{
|
|
4
|
+
const client = await pool.connect()
|
|
5
|
+
res.locals.client = client
|
|
6
|
+
next()
|
|
7
|
+
} catch(e){
|
|
8
|
+
console.error(e)
|
|
9
|
+
res.status(400)
|
|
10
|
+
res.json({error:e.message})
|
|
11
|
+
client.release()
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
return app
|
|
15
|
+
}
|
|
16
|
+
//dynamically add routes based on ./routes/index.js
|
|
17
|
+
// return {status:<status>, message:<message>} from each route
|
|
18
|
+
// JSON, strings and files are supported, files must be an absolute path
|
|
19
|
+
|
|
20
|
+
const dynamicRoutes = (app, router) => {
|
|
21
|
+
Object.values(router).forEach(({method, route, handler})=>{
|
|
22
|
+
app[method](route, async(req, res)=>{
|
|
23
|
+
try{
|
|
24
|
+
const returnVal = await handler(req, res.locals.client)
|
|
25
|
+
if(returnVal instanceof Function){
|
|
26
|
+
returnVal(res)
|
|
27
|
+
}else{
|
|
28
|
+
if(typeof returnVal.message === 'string'){
|
|
29
|
+
if(returnVal.message.startsWith('/')){
|
|
30
|
+
res.status(returnVal.status)
|
|
31
|
+
res.sendFile(returnVal.message)
|
|
32
|
+
}else{
|
|
33
|
+
res.status(returnVal.status)
|
|
34
|
+
res.send(returnVal.message)
|
|
35
|
+
res.end()
|
|
36
|
+
}
|
|
37
|
+
}else if(typeof returnVal.message === 'object'){
|
|
38
|
+
res.status(returnVal.status)
|
|
39
|
+
res.json(returnVal.message)
|
|
40
|
+
res.end()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch(e){
|
|
44
|
+
console.error(e)
|
|
45
|
+
res.status(501)
|
|
46
|
+
res.end()
|
|
47
|
+
}finally{
|
|
48
|
+
console.log('end')
|
|
49
|
+
if(res.locals.client){
|
|
50
|
+
res.locals.client.release()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const defaultServer = (express, pool, router, config={apiBaseRoute:'/api'}) => {
|
|
59
|
+
const app = express()
|
|
60
|
+
app.use(express.json({ limit: '5mb' }));
|
|
61
|
+
app.use(express.urlencoded({ extended: true })); // uses qs when true
|
|
62
|
+
|
|
63
|
+
addDbOnApiRoute(app, pool, {baseRoute:config.apiBaseRoute})
|
|
64
|
+
dynamicRoutes(app, router)
|
|
65
|
+
|
|
66
|
+
const server = app.listen(process.env.PORT,process.env.HOST)
|
|
67
|
+
|
|
68
|
+
console.log('port', process.env.PORT)
|
|
69
|
+
|
|
70
|
+
return {app, server}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
dynamicRoutes,
|
|
76
|
+
addDbOnApiRoute,
|
|
77
|
+
defaultServer
|
|
78
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { objectToUriParams } = require('../util.js')
|
|
2
|
+
const { encrypt, decrypt } = require('../ncrypt.js')
|
|
3
|
+
const { getSecretName, getSecret } = require('../secret.js')
|
|
4
|
+
|
|
5
|
+
// this is not actually a test framework
|
|
6
|
+
// all the functions are compatible with jest though
|
|
7
|
+
const describe = (name, fn) => {
|
|
8
|
+
console.log(`describe: ${name}`)
|
|
9
|
+
fn()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const it = (name, fn) => {
|
|
13
|
+
console.log(`it: ${name}`)
|
|
14
|
+
fn()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const expect = (result) => {
|
|
18
|
+
return {
|
|
19
|
+
toBe: (expected) => {
|
|
20
|
+
if (result !== expected) {
|
|
21
|
+
throw new Error(`Expected ${result} to be ${expected}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//end mocks for jests
|
|
28
|
+
|
|
29
|
+
describe('objectToUriParams', () => {
|
|
30
|
+
it('should convert an object to a URI string', () => {
|
|
31
|
+
const obj = {
|
|
32
|
+
a: 1,
|
|
33
|
+
b: 2,
|
|
34
|
+
c: 'hello world',
|
|
35
|
+
d: ' !@#$%^&*()_+-=[]{}|;:\'",.<>/?`~\n\t\r'
|
|
36
|
+
}
|
|
37
|
+
const result = objectToUriParams(obj)
|
|
38
|
+
|
|
39
|
+
const expected = [
|
|
40
|
+
`a=${encodeURIComponent(obj.a)}`,
|
|
41
|
+
`b=${encodeURIComponent(obj.b)}`,
|
|
42
|
+
`c=${encodeURIComponent(obj.c)}`,
|
|
43
|
+
`d=${encodeURIComponent(obj.d)}`
|
|
44
|
+
].join('&')
|
|
45
|
+
expect(result).toBe(expected)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('encrypt', () => {
|
|
50
|
+
it('works', async () => {
|
|
51
|
+
const toEncrypt = {
|
|
52
|
+
access_token: 'averylongnotrealaccesstokensG0Ttoopo44t0xJfj1bljseyyHL0W0by3-Zjpu5tlAoHN_PKOelWTI7hwvvNkEdrM-jnKtUszuMlS7nAK3Y0x-m0ptTHoeVPZPE5jl-NNT52q17qZn0z0tlRBzHIpyKEiiofQ6mnUy7Lrz-hrXxvTWV_ug0HG6FqYruk-tmSUZJkKMwsdcWS1xZKdd5h8oxYScm5-gLnwd3Zal9-NTQCEob5WyBBBWU7PsNVmewesqpEOaQDanT8jiyn7ul0XJhLS1141o5_GeD1Z9gw-tmWqtlZO13O9Miz-1gDD-lNsWTKF3KkneqoAUUsMPegz_R5Cax43UYTXoTqahT4lJEYx_tbLiDAVel5FIgU6KrB5zS9lOv0_o42nunH-xmX8OWrU6l5yPShr9xiXkuyi2_Yv4y8QW5sAYMg4G4I8iJON5pH8RuPPtnSOwq7KNp7EBCb7u_St2dOcVRYCZjK5RgJK0jcp4PspUbNRNLenAbAHRe9u8PKilmo1iVafV2EmobC3WweaTF_Tbbc1U7tQEUuYnAES2KOSugsyDXNBMv283LkIjCaRW11jSA12_nubWBBHu_zxdSS3j61TR8DCkWppzd_1Um2n9kK3RB7q94wh0IANZxFJ4cS8vUvUU8Wphq4QMxIng9a-IkN99aKsIgRI95HB-eVvcbFT6jqpUpgmZgBS8uvIULmgVT-1Zs6C4UKrN16qWfc5MR_wnNjSIcIiP-pUkVmLmqOOMZM9llpD-eMrgS17xDG7lwSrFmZEEHQeaVWNhBOylivToBPMvMF5vunVNpaZas4N_1tzCP3MhH00LIYjCFoBcpPvV5IgHD3peF9R_qrMnsev2n4nY4C25IXK3cCldFw0uSDRL1rEqkaxFYwsKOqfVNI6YO8om_p8IuoD9yJ15aeZJPwsqZpEFcV04Ag1e6GMC5LmiqQ2u1iRyrbkBADChqTBabo3bU_hfwaureOtmUKkYFDymKolLxsHj1FZwqzH5qmi95yewGs_UpX81tQqZopAJb5othTKw4J_5yt3LBm7LW1uNGrYpulC38m8hU9pwKq3mj8v95H9fyONLlEUMKOctzDBxgZlV5EEGnfXYuWEvv9Vv3QuULSEj_fvcuXgDfKp_y2JB6NMV57Dhn5xkFMLpstXk9R97IIMI8tSED0A',
|
|
53
|
+
token_type: 'bearer',
|
|
54
|
+
expires_in: 14400,
|
|
55
|
+
scope: 'account_info.read',
|
|
56
|
+
uid: '2706081665',
|
|
57
|
+
account_id: 'dbid:OIUYZb6mgUwwAFf5xJeb-8zFExUJKqtrnDo'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
const secretName = await getSecretName()
|
|
62
|
+
const secret = await getSecret(secretName)
|
|
63
|
+
const encrypted = encrypt(JSON.stringify(toEncrypt), secret.publicKey)
|
|
64
|
+
const decrypted = decrypt(encrypted, secret.privateKey)
|
|
65
|
+
expect(decrypted).toBe(JSON.stringify(toEncrypt))
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick test to verify the rate limit retry logic works correctly
|
|
3
|
+
* Run with: node tests/rate-limit-test.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Dropbox = require('../dropbox.js')
|
|
7
|
+
|
|
8
|
+
// Mock axios that returns 429 twice, then succeeds
|
|
9
|
+
const createMockAxios = () => {
|
|
10
|
+
let callCount = 0
|
|
11
|
+
return {
|
|
12
|
+
post: async (url, data, config) => {
|
|
13
|
+
callCount++
|
|
14
|
+
console.log(`[Mock] API call #${callCount} to ${url}`)
|
|
15
|
+
|
|
16
|
+
if (callCount <= 2) {
|
|
17
|
+
// Simulate 429 rate limit for first 2 calls
|
|
18
|
+
const error = new Error('Rate limited')
|
|
19
|
+
error.response = { status: 429 }
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Success on 3rd call
|
|
24
|
+
return { data: { link: 'https://example.com/temp-link' } }
|
|
25
|
+
},
|
|
26
|
+
get: async (url, config) => {
|
|
27
|
+
// Mock getAccessToken
|
|
28
|
+
return { data: { access_token: 'mock-token' } }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const runTest = async () => {
|
|
34
|
+
console.log('=== Testing Rate Limit Retry Logic ===\n')
|
|
35
|
+
|
|
36
|
+
const mockAxios = createMockAxios()
|
|
37
|
+
const dropbox = Dropbox(mockAxios)
|
|
38
|
+
|
|
39
|
+
// Set a mock token
|
|
40
|
+
dropbox.setToken('mock-token')
|
|
41
|
+
|
|
42
|
+
console.log('Calling getTemporaryLink (should fail twice, then succeed)...\n')
|
|
43
|
+
|
|
44
|
+
const startTime = Date.now()
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = await dropbox.getTemporaryLink({ path: '/test/file.jpg' })
|
|
48
|
+
const elapsed = Date.now() - startTime
|
|
49
|
+
|
|
50
|
+
console.log(`\n✅ SUCCESS! Got result after ${elapsed}ms:`, result)
|
|
51
|
+
console.log('\nThe retry logic is working correctly!')
|
|
52
|
+
console.log('- First call: 429 → waited ~1000ms → retried')
|
|
53
|
+
console.log('- Second call: 429 → waited ~2000ms → retried')
|
|
54
|
+
console.log('- Third call: Success!')
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log('\n❌ FAILED:', error.message)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
runTest()
|