@goniglep57/node 1.0.1 → 1.0.3

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/awsSecrets.js ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * AWS Secrets Manager utility for fetching secrets at runtime.
3
+ *
4
+ * This module provides a centralized way to fetch secrets from AWS Secrets Manager,
5
+ * eliminating the need to manually set secrets via Heroku config vars.
6
+ *
7
+ * Usage:
8
+ * const { awsSecrets } = require('@goniglep57/node')
9
+ *
10
+ * // At app startup:
11
+ * await awsSecrets.init()
12
+ *
13
+ * // Then use secrets:
14
+ * const apiKey = awsSecrets.get('INTERNAL_API_KEY')
15
+ *
16
+ * Required environment variables:
17
+ * - AWS_ACCESS_KEY_ID_SECRETS (or AWS_ACCESS_KEY_ID)
18
+ * - AWS_SECRET_ACCESS_KEY_SECRETS (or AWS_SECRET_ACCESS_KEY)
19
+ * - AWS_REGION (optional, defaults to 'us-east-1')
20
+ */
21
+
22
+ const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager')
23
+
24
+ // Cache for secrets (keyed by secret name)
25
+ const secretsCache = {}
26
+
27
+ // Default secret name for internal API keys
28
+ const DEFAULT_SECRET_NAME = 'prod/internal'
29
+
30
+ /**
31
+ * Get AWS credentials from environment variables.
32
+ * Supports both dedicated secrets credentials and fallback to general AWS credentials.
33
+ */
34
+ const getCredentials = () => {
35
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID_SECRETS || process.env.AWS_ACCESS_KEY_ID
36
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY_SECRETS || process.env.AWS_SECRET_ACCESS_KEY
37
+
38
+ if (!accessKeyId || !secretAccessKey) {
39
+ throw new Error(
40
+ 'AWS credentials not configured. Set AWS_ACCESS_KEY_ID_SECRETS and AWS_SECRET_ACCESS_KEY_SECRETS ' +
41
+ '(or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) environment variables.'
42
+ )
43
+ }
44
+
45
+ return { accessKeyId, secretAccessKey }
46
+ }
47
+
48
+ /**
49
+ * Create a Secrets Manager client with configured credentials.
50
+ */
51
+ const createClient = () => {
52
+ const credentials = getCredentials()
53
+ const region = process.env.AWS_REGION || 'us-east-1'
54
+
55
+ return new SecretsManagerClient({ region, credentials })
56
+ }
57
+
58
+ /**
59
+ * Fetch a secret from AWS Secrets Manager.
60
+ * Results are cached to avoid repeated API calls.
61
+ *
62
+ * @param {string} secretName - The name/ARN of the secret in Secrets Manager
63
+ * @returns {Promise<object>} - The parsed secret value (assumes JSON)
64
+ */
65
+ const fetchSecret = async (secretName = DEFAULT_SECRET_NAME) => {
66
+ // Return cached value if available
67
+ if (secretsCache[secretName]) {
68
+ return secretsCache[secretName]
69
+ }
70
+
71
+ const client = createClient()
72
+
73
+ try {
74
+ const command = new GetSecretValueCommand({ SecretId: secretName })
75
+ const response = await client.send(command)
76
+
77
+ // Parse the secret (assumes JSON format)
78
+ const secretValue = JSON.parse(response.SecretString)
79
+
80
+ // Cache the result
81
+ secretsCache[secretName] = secretValue
82
+
83
+ console.log(`[awsSecrets] Loaded secret: ${secretName}`)
84
+ return secretValue
85
+ } catch (error) {
86
+ console.error(`[awsSecrets] Failed to fetch secret '${secretName}':`, error.message)
87
+ throw error
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Initialize secrets by fetching the default secret.
93
+ * Call this at app startup before using any secrets.
94
+ *
95
+ * @param {string|string[]} secretNames - Secret name(s) to fetch (defaults to 'prod/internal')
96
+ * @returns {Promise<void>}
97
+ */
98
+ const init = async (secretNames = DEFAULT_SECRET_NAME) => {
99
+ const names = Array.isArray(secretNames) ? secretNames : [secretNames]
100
+
101
+ for (const name of names) {
102
+ await fetchSecret(name)
103
+ }
104
+
105
+ console.log(`[awsSecrets] Initialized with ${names.length} secret(s)`)
106
+ }
107
+
108
+ /**
109
+ * Get a specific key from a cached secret.
110
+ *
111
+ * @param {string} key - The key to retrieve from the secret
112
+ * @param {string} secretName - The secret name (defaults to 'prod/internal')
113
+ * @returns {string|undefined} - The secret value, or undefined if not found
114
+ */
115
+ const get = (key, secretName = DEFAULT_SECRET_NAME) => {
116
+ const secret = secretsCache[secretName]
117
+
118
+ if (!secret) {
119
+ console.warn(`[awsSecrets] Secret '${secretName}' not loaded. Call init() first.`)
120
+ return undefined
121
+ }
122
+
123
+ return secret[key]
124
+ }
125
+
126
+ /**
127
+ * Get the entire cached secret object.
128
+ *
129
+ * @param {string} secretName - The secret name (defaults to 'prod/internal')
130
+ * @returns {object|undefined} - The full secret object, or undefined if not loaded
131
+ */
132
+ const getAll = (secretName = DEFAULT_SECRET_NAME) => {
133
+ return secretsCache[secretName]
134
+ }
135
+
136
+ /**
137
+ * Clear the secrets cache (useful for testing or forcing refresh).
138
+ */
139
+ const clearCache = () => {
140
+ Object.keys(secretsCache).forEach(key => delete secretsCache[key])
141
+ console.log('[awsSecrets] Cache cleared')
142
+ }
143
+
144
+ /**
145
+ * Check if secrets are initialized.
146
+ *
147
+ * @param {string} secretName - The secret name to check (defaults to 'prod/internal')
148
+ * @returns {boolean}
149
+ */
150
+ const isInitialized = (secretName = DEFAULT_SECRET_NAME) => {
151
+ return !!secretsCache[secretName]
152
+ }
153
+
154
+ module.exports = {
155
+ init,
156
+ fetchSecret,
157
+ get,
158
+ getAll,
159
+ clearCache,
160
+ isInitialized,
161
+ DEFAULT_SECRET_NAME
162
+ }
package/dropbox.js CHANGED
@@ -91,9 +91,9 @@ const Dropbox = (httpClient, config={retry:false}) => {
91
91
  console.error('Path not found in dropbox', body)
92
92
  return null
93
93
  }
94
- // throw err
95
- console.error(err.message)
96
- // throw err
94
+ // Log detailed Dropbox API error instead of generic HTTP status
95
+ console.error('Dropbox API error:', err.response?.data?.error_summary || err.message, body)
96
+ return null
97
97
  }
98
98
  }
99
99
 
package/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  //generator:exportdir
2
2
  exports.alerts = require('./alerts.js')
3
+ exports.awsSecrets = require('./awsSecrets.js')
3
4
  exports.dropbox = require('./dropbox.js')
4
5
  exports.ncrypt = require('./ncrypt.js')
5
6
  exports.secret = require('./secret.js')
6
7
  exports.server = require('./server.js')
7
- exports.slack = require('./slack.js')
8
8
  exports.util = require('./util.js')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goniglep57/node",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A package full of utility functions",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@aws-sdk/client-s3": "^3.957.0",
13
+ "@aws-sdk/client-secrets-manager": "^3.957.0",
13
14
  "@aws-sdk/client-sqs": "^3.957.0",
14
15
  "nodemailer": "^6.9.16"
15
16
  }
package/slack.js DELETED
@@ -1,184 +0,0 @@
1
- /**
2
- * Slack notification module for error alerting across all services
3
- *
4
- * Usage:
5
- * const { slack } = require('@goniglep57/node')
6
- *
7
- * // Configure once at startup
8
- * slack.configure({
9
- * webhookUrl: process.env.SLACK_ERROR_WEBHOOK,
10
- * serviceName: 'tonomo-front-end',
11
- * environment: process.env.NODE_ENV || 'development'
12
- * })
13
- *
14
- * // Send error notifications
15
- * await slack.error('Database connection failed', { userId: 123 })
16
- * await slack.warn('Rate limit approaching', { current: 90, max: 100 })
17
- * await slack.info('Deployment complete')
18
- */
19
-
20
- let config = null
21
-
22
- /**
23
- * Configure the Slack module
24
- * @param {Object} options
25
- * @param {string} options.webhookUrl - Slack incoming webhook URL
26
- * @param {string} options.serviceName - Name of the service (e.g., 'tonomo-front-end')
27
- * @param {string} [options.environment='production'] - Environment name
28
- */
29
- const configure = (options) => {
30
- const { webhookUrl, serviceName, environment = 'production' } = options
31
-
32
- if (!webhookUrl) {
33
- console.warn('[slack] No webhook URL provided, notifications will be disabled')
34
- config = null
35
- return { configured: false }
36
- }
37
-
38
- if (!serviceName) {
39
- throw new Error('slack.configure requires serviceName')
40
- }
41
-
42
- config = {
43
- webhookUrl,
44
- serviceName,
45
- environment
46
- }
47
-
48
- return { configured: true }
49
- }
50
-
51
- /**
52
- * Check if Slack is configured
53
- */
54
- const isConfigured = () => config !== null
55
-
56
- /**
57
- * Send a notification to Slack
58
- * @param {string} level - 'error', 'warn', or 'info'
59
- * @param {string} message - The message
60
- * @param {Object} [context={}] - Additional context
61
- */
62
- const send = async (level, message, context = {}) => {
63
- if (!config) {
64
- return { sent: false, reason: 'not configured' }
65
- }
66
-
67
- const emoji = {
68
- error: '🚨',
69
- warn: 'âš ī¸',
70
- info: 'â„šī¸'
71
- }[level] || 'đŸ“ĸ'
72
-
73
- const color = {
74
- error: '#dc3545',
75
- warn: '#ffc107',
76
- info: '#17a2b8'
77
- }[level] || '#6c757d'
78
-
79
- try {
80
- const contextLines = Object.entries(context)
81
- .filter(([_, v]) => v !== undefined && v !== null)
82
- .map(([key, value]) => {
83
- const displayValue = typeof value === 'object'
84
- ? JSON.stringify(value).substring(0, 200)
85
- : String(value).substring(0, 200)
86
- return `â€ĸ *${key}:* ${displayValue}`
87
- })
88
- .join('\n')
89
-
90
- const payload = {
91
- attachments: [
92
- {
93
- color,
94
- blocks: [
95
- {
96
- type: 'header',
97
- text: {
98
- type: 'plain_text',
99
- text: `${emoji} ${config.serviceName} - ${level.toUpperCase()}`,
100
- emoji: true
101
- }
102
- },
103
- {
104
- type: 'section',
105
- text: {
106
- type: 'mrkdwn',
107
- text: `*Message:*\n${message}`
108
- }
109
- }
110
- ]
111
- }
112
- ]
113
- }
114
-
115
- if (contextLines) {
116
- payload.attachments[0].blocks.push({
117
- type: 'section',
118
- text: {
119
- type: 'mrkdwn',
120
- text: `*Context:*\n${contextLines}`
121
- }
122
- })
123
- }
124
-
125
- payload.attachments[0].blocks.push({
126
- type: 'context',
127
- elements: [
128
- {
129
- type: 'mrkdwn',
130
- text: `🌍 ${config.environment} | ⏰ ${new Date().toISOString()}`
131
- }
132
- ]
133
- })
134
-
135
- const response = await fetch(config.webhookUrl, {
136
- method: 'POST',
137
- headers: { 'Content-Type': 'application/json' },
138
- body: JSON.stringify(payload)
139
- })
140
-
141
- if (!response.ok) {
142
- console.error('[slack] Failed to send notification:', response.status)
143
- return { sent: false, reason: `HTTP ${response.status}` }
144
- }
145
-
146
- return { sent: true }
147
- } catch (err) {
148
- // Don't let Slack errors break the main flow
149
- console.error('[slack] Error sending notification:', err.message)
150
- return { sent: false, reason: err.message }
151
- }
152
- }
153
-
154
- /**
155
- * Send an error notification
156
- */
157
- const error = (message, context = {}) => send('error', message, context)
158
-
159
- /**
160
- * Send a warning notification
161
- */
162
- const warn = (message, context = {}) => send('warn', message, context)
163
-
164
- /**
165
- * Send an info notification
166
- */
167
- const info = (message, context = {}) => send('info', message, context)
168
-
169
- /**
170
- * Reset configuration (useful for testing)
171
- */
172
- const reset = () => {
173
- config = null
174
- }
175
-
176
- module.exports = {
177
- configure,
178
- isConfigured,
179
- send,
180
- error,
181
- warn,
182
- info,
183
- reset
184
- }