@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 +162 -0
- package/dropbox.js +3 -3
- package/index.js +1 -1
- package/package.json +2 -1
- package/slack.js +0 -184
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
|
-
//
|
|
95
|
-
console.error(err.message)
|
|
96
|
-
|
|
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.
|
|
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
|
-
}
|