@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 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
@@ -0,0 +1,12 @@
1
+ # Common packages
2
+
3
+ These are common packages that are used across the monorepo
4
+
5
+ ## env required
6
+
7
+ ### secret.js
8
+
9
+ Production requirement:
10
+ PRODUCTION_SECRET_NAME
11
+ PRODUCTION_PUBLIC_KEY
12
+ PRODUCTION_PRIVATE_KEY
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()
package/util.js ADDED
@@ -0,0 +1,11 @@
1
+
2
+ const objectToUriParams = obj => {
3
+ return Object.entries(obj)
4
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
5
+ .join('&')
6
+ }
7
+
8
+
9
+ module.exports = {
10
+ objectToUriParams
11
+ }