diff --git a/docker/.env b/docker/.env
index a013eee..cf59ab0 100644
--- a/docker/.env
+++ b/docker/.env
@@ -55,6 +55,11 @@ CPX_DB_CONNECTION_STRING=postgres://${CPX_DB_USERNAME}:${CPX_DB_PASSWORD}@capxml
CPX_REDIS_HOST=cap-xml-redis
CPX_REDIS_PORT=6379
CPX_REDIS_TLS=false
+CPX_METEOALARM_API_URL=http://mock-api:8080 # wiremock url
+CPX_METEOALARM_API_USERNAME=username
+CPX_METEOALARM_API_PASSWORD=password
+CPX_METEOALARM_DISABLE=false
+NODE_TLS_REJECT_UNAUTHORIZED=0
PGADMIN_DEFAULT_PASSWORD=pgadmin
POSTGRES_PASSWORD=postgres
LIQUIBASE_COMMAND_CHANGELOG_FILE=./changelog/db.changelog-master.xml
diff --git a/docker/dev-tools.yml b/docker/dev-tools.yml
index 8a8b3bb..6363160 100644
--- a/docker/dev-tools.yml
+++ b/docker/dev-tools.yml
@@ -28,6 +28,15 @@ services:
networks:
ls:
command: /bin/sh -c "lpm add postgresql && liquibase update"
+ mock-api:
+ image: wiremock/wiremock:latest
+ ports:
+ - "8081:8080"
+ volumes:
+ - ./wiremock/mappings:/home/wiremock/mappings
+ command: ["--global-response-templating", "--verbose"]
+ networks:
+ ls:
volumes:
capxmlpgadmin:
external: true
diff --git a/docker/scripts/register-lambda-functions.sh b/docker/scripts/register-lambda-functions.sh
index 7acde00..3e40b79 100755
--- a/docker/scripts/register-lambda-functions.sh
+++ b/docker/scripts/register-lambda-functions.sh
@@ -16,7 +16,12 @@ cpx_agw_url=$(echo CPX_AGW_URL=$deployed_cpx_agw_url)
cpx_redis_host=$(echo CPX_REDIS_HOST=$CPX_REDIS_HOST)
cpx_redis_port=$(echo CPX_REDIS_PORT=$CPX_REDIS_PORT)
cpx_redis_tls=$(echo CPX_REDIS_TLS=$CPX_REDIS_TLS)
-set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host $cpx_redis_port $cpx_redis_tls
+cpx_meteoalarm_api_url=$(echo CPX_METEOALARM_API_URL=$CPX_METEOALARM_API_URL)
+cpx_meteoalarm_api_username=$(echo CPX_METEOALARM_API_USERNAME=$CPX_METEOALARM_API_USERNAME)
+cpx_meteoalarm_api_password=$(echo CPX_METEOALARM_API_PASSWORD=$CPX_METEOALARM_API_PASSWORD)
+cpx_meteoalarm_disable=$(echo CPX_METEOALARM_DISABLE=$CPX_METEOALARM_DISABLE)
+node_tls_reject_unauthorized=$(echo NODE_TLS_REJECT_UNAUTHORIZED=$NODE_TLS_REJECT_UNAUTHORIZED)
+set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host $cpx_redis_port $cpx_redis_tls $cpx_meteoalarm_api_url $cpx_meteoalarm_api_username $cpx_meteoalarm_api_password $node_tls_reject_unauthorized $cpx_meteoalarm_disable
custom_environment_variables=$(printf '%s,' "$@" | sed 's/,*$//g')
# Iterate over each file in lambda_functions_dir
diff --git a/docker/wiremock/mappings/third-party-api.json b/docker/wiremock/mappings/third-party-api.json
new file mode 100644
index 0000000..0c198b1
--- /dev/null
+++ b/docker/wiremock/mappings/third-party-api.json
@@ -0,0 +1,24 @@
+{
+ "mappings": [
+ {
+ "request": {
+ "method": "POST",
+ "urlPath": "/warnings"
+ },
+ "response": {
+ "status": 201,
+ "body": "{\"warning\": {\"uuid\": \"{{randomValue type='UUID'}}\" } }"
+ }
+ },
+ {
+ "request": {
+ "method": "POST",
+ "urlPath": "/tokens"
+ },
+ "response": {
+ "status": 200,
+ "body": "{\"tokenType\": \"Bearer\", \"token\": \"{{randomValue length=64 type='ALPHANUMERIC'}}\", \"expiresIn\": \"300\" }"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/functions/archiveMessages.js b/lib/functions/archiveMessages.js
index f3faa50..402d4cd 100644
--- a/lib/functions/archiveMessages.js
+++ b/lib/functions/archiveMessages.js
@@ -3,6 +3,7 @@
const service = require('../helpers/service')
module.exports.archiveMessages = async () => {
- console.log('archiving messages')
+ console.log('[archiveMessages] Starting to archive messages')
await service.archiveMessages()
+ console.log('[archiveMessages] Finished archiving messages')
}
diff --git a/lib/functions/processMessage.js b/lib/functions/processMessage.js
index ee04749..b1e2bd5 100644
--- a/lib/functions/processMessage.js
+++ b/lib/functions/processMessage.js
@@ -11,45 +11,58 @@ const path = require('node:path')
const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8')
const additionalCapMessageSchema = require('../schemas/additionalCapMessageSchema')
const Message = require('../models/message')
-const EA_WHO = '2.49.0.0.826.1'
+const EA_WHO = '2.49.0.1.826.1'
const CODE = 'MCP:v2.0'
const severityV2Mapping = require('../models/v2MessageMapping')
const redis = require('../helpers/redis')
+const meteoalarm = require('../helpers/meteoalarm')
module.exports.processMessage = async (event) => {
+ console.log('[processMessage] Event received:', event)
try {
// validate the event
await eventSchema.validateAsync(event)
// parse the xml
const message = new Message(event.bodyXml)
- console.log(`Processing CAP message: ${message.identifier} for ${message.fwisCode}`)
+ console.log(`[processMessage] Processing CAP message: ${message.identifier} for ${message.fwisCode}`)
// get Last message
const dbResult = await service.getLastMessage(message.fwisCode)
const lastMessage = (!!dbResult && dbResult.rows.length > 0) ? dbResult.rows[0] : undefined
+ if (lastMessage) {
+ console.log(`[processMessage] Found last message for ${message.fwisCode}: identifier=${lastMessage.identifier}, expires=${lastMessage.expires}, status=${lastMessage.status}`)
+ } else {
+ console.log(`[processMessage] No previous message found for ${message.fwisCode}`)
+ }
+
// If not production set status to test
if (process.env.stage !== 'prd') {
message.status = 'Test'
}
+ console.log(`[processMessage] Running in stage: ${process.env.stage}, message status set to: ${message.status}`)
// Add in the references field and update msgtype to Update if references exist and is Alert (does this in message model)
const references = buildReference(lastMessage, message.sender, 'identifier', 'references')
if (references) {
message.references = references
+ console.log(`[processMessage] Built references: ${references}, msgType updated to: ${message.msgType}`)
}
// Generate message V2 for meteoalarm spec
const messageV2 = processMessageV2(message, lastMessage)
// do validation against OASIS CAP xml schema and extended JOI schema
+ console.log(`[processMessage] Starting validation for ${message.identifier}`)
+ const validationStart = Date.now()
const results = await Promise.allSettled([
validateAgainstXsdSchema(message),
validateAgainstJoiSchema(message),
validateAgainstXsdSchema(messageV2),
validateAgainstJoiSchema(messageV2)
])
+ console.log(`[processMessage] Validation completed in ${Date.now() - validationStart}ms for ${message.identifier}`)
// Check for validation failures and throw
const errors = results.filter(r => r.status === 'rejected').flatMap(r => r.reason)
@@ -58,9 +71,22 @@ module.exports.processMessage = async (event) => {
}
const { message: redisMessage, query: dbQuery } = message.putQuery(message, messageV2)
- // store the message in database and redis/elasticache
- await Promise.all([service.putMessage(dbQuery), redis.set(redisMessage.identifier, redisMessage)])
- console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`)
+ // store the message in database, redis/elasticache, and post to Meteoalarm
+ const storageStart = Date.now()
+ const promises = [
+ service.putMessage(dbQuery),
+ redis.set(redisMessage.identifier, redisMessage)
+ ]
+ if (process.env.CPX_METEOALARM_DISABLE === 'true') {
+ console.log('[processMessage] Meteoalarm integration is disabled')
+ } else {
+ promises.push(meteoalarm.postWarning(messageV2.toString(), message.identifier))
+ }
+ await Promise.all(promises)
+ console.log(`[processMessage] Storage operations completed in ${Date.now() - storageStart}ms`)
+ console.log(`[processMessage] DB write successful for ${message.identifier}`)
+ console.log(`[processMessage] Redis cache set for ${message.identifier}`)
+ console.log(`[processMessage] Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`)
return {
statusCode: 200,
@@ -76,7 +102,9 @@ module.exports.processMessage = async (event) => {
} catch (err) {
// Actual error will be handled by lambda process
// So just log the message body to console for investigation
- console.log(event.bodyXml)
+ console.error(`[processMessage] Error during processing: ${err.message}`)
+ console.error(`[processMessage] Error stack: ${err.stack}`)
+ console.log('[processMessage] Failed message body:', event.bodyXml)
return processFailedMessage(err, event.bodyXml)
}
}
@@ -167,6 +195,7 @@ const processMessageV2 = (message, lastMessage) => {
messageV2.references = referencesV2
}
messageV2.event = `${severityV2Mapping[message.severity]?.description}: ${messageV2.areaDesc}`
+ messageV2.responseType = 'Monitor'
messageV2.severity = severityV2Mapping[message.severity]?.severity || ''
messageV2.onset = message.sent
messageV2.headline = `${severityV2Mapping[message.severity]?.headline}: ${messageV2.areaDesc}`
@@ -188,5 +217,7 @@ const processMessageV2 = (message, lastMessage) => {
messageV2.addParameter('use_polygon_over_geocode', 'true')
messageV2.addParameter('uk_ea_ta_code', message.fwisCode)
+ messageV2.removeNode('geocode')
+
return messageV2
}
diff --git a/lib/helpers/message.js b/lib/helpers/message.js
index e832357..b6b0f27 100644
--- a/lib/helpers/message.js
+++ b/lib/helpers/message.js
@@ -8,31 +8,45 @@ const path = require('node:path')
const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8')
const redis = require('../helpers/redis')
+const fetchMessageBody = async (key, v2) => {
+ const fetchStart = Date.now()
+ const cachedMessage = await redis.get(key)
+ let body = {}
+
+ if (cachedMessage) {
+ console.log(`[getMessage] Cache HIT for ${key}`)
+ body = v2 ? cachedMessage.alert_v2 : cachedMessage.alert
+ console.log(`[getMessage] Message retrieved in ${Date.now() - fetchStart}ms for ${key}`)
+ return body
+ }
+
+ console.log(`[getMessage] Cache MISS for ${key}, fetching from database`)
+ const ret = await service.getMessage(key)
+ if (!ret?.rows || !Array.isArray(ret.rows) || ret.rows.length < 1 || !ret.rows[0].getmessage) {
+ console.log('[getMessage] No message found for ' + key)
+ throw new Error('No message found')
+ }
+ const message = ret.rows[0].getmessage
+ body = v2 ? message.alert_v2 : message.alert
+ // Cache the message in redis
+ await redis.set(key, message)
+ console.log(`[getMessage] Retrieved from database and cached: ${key}`)
+ console.log(`[getMessage] Message retrieved in ${Date.now() - fetchStart}ms for ${key}`)
+ return body
+}
+
module.exports.getMessage = async (event, v2) => {
+ console.log('[getMessage] Event received:', event)
const { error } = eventSchema.validate(event)
if (error) {
throw error
}
- // Fetch message from redis, else get from postgres
- let body
const key = event.pathParameters.id
- const cachedMessage = await redis.get(key)
+ console.log(`[getMessage] Fetching message with id: ${key}, version: ${v2 ? 'v2' : 'v1'}`)
- if (cachedMessage) {
- body = v2 ? cachedMessage.alert_v2 : cachedMessage.alert
- } else {
- const ret = await service.getMessage(key)
- if (!ret?.rows || !Array.isArray(ret.rows) || ret.rows.length < 1 || !ret.rows[0].getmessage) {
- console.log('No message found for ' + key)
- throw new Error('No message found')
- }
- const message = ret.rows[0].getmessage
- body = v2 ? message.alert_v2 : message.alert
- // Cache the message in redis
- await redis.set(key, message)
- }
+ const body = await fetchMessageBody(key, v2)
const validationResult = await validateXML({
xml: [{
@@ -44,10 +58,11 @@ module.exports.getMessage = async (event, v2) => {
// NI-95 log validation errors and continue processing
if (validationResult.errors?.length > 0) {
- console.log('CAP get message failed validation')
- console.log(JSON.stringify(validationResult.errors))
+ console.log('[getMessage] CAP get message failed validation')
+ console.log('[getMessage] Validation errors:', JSON.stringify(validationResult.errors))
}
+ console.log(`[getMessage] Returning message ${key}, size: ${body.length} bytes`)
return {
statusCode: 200,
headers: {
diff --git a/lib/helpers/messages.js b/lib/helpers/messages.js
index a6036f4..f8b36e6 100644
--- a/lib/helpers/messages.js
+++ b/lib/helpers/messages.js
@@ -6,8 +6,14 @@ const path = require('node:path')
const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'atom.xsd'), 'utf8')
module.exports.messages = async (v2 = false) => {
+ console.log(`[getMessagesAtom] Generating atom feed, version: ${v2 ? 'v2' : 'v1'}`)
+ const feedStart = Date.now()
const { Feed } = await import('feed')
+ const dbStart = Date.now()
const ret = await service.getAllMessages()
+ console.log(`[getMessagesAtom] Database query completed in ${Date.now() - dbStart}ms`)
+ const messageCount = ret?.rows?.length || 0
+ console.log(`[getMessagesAtom] Feed contains ${messageCount} messages`)
const uriPrefix = v2 ? '/v2' : ''
const feed = new Feed({
@@ -41,7 +47,9 @@ module.exports.messages = async (v2 = false) => {
}
}
+ const feedGenStart = Date.now()
const xmlFeed = feed.atom1()
+ console.log(`[getMessagesAtom] Feed generated in ${Date.now() - feedGenStart}ms, size: ${xmlFeed.length} bytes`)
const validationResult = await validateXML({
xml: [{
@@ -52,10 +60,11 @@ module.exports.messages = async (v2 = false) => {
})
// NI-95 log validation errors and continue processing
if (validationResult.errors?.length > 0) {
- console.log('ATOM feed failed validation')
- console.log(JSON.stringify(validationResult.errors))
+ console.log('[getMessagesAtom] ATOM feed failed validation')
+ console.log('[getMessagesAtom] Validation errors:', JSON.stringify(validationResult.errors))
}
+ console.log(`[getMessagesAtom] Total feed generation time: ${Date.now() - feedStart}ms`)
return {
statusCode: 200,
headers: {
diff --git a/lib/helpers/meteoalarm.js b/lib/helpers/meteoalarm.js
new file mode 100644
index 0000000..1c587ce
--- /dev/null
+++ b/lib/helpers/meteoalarm.js
@@ -0,0 +1,110 @@
+'use strict'
+
+const axios = require('axios')
+let cachedToken = null
+let tokenExpiry = null
+const CPX_METEOALARM_API_URL = process.env.CPX_METEOALARM_API_URL
+const CPX_METEOALARM_API_USERNAME = process.env.CPX_METEOALARM_API_USERNAME
+const CPX_METEOALARM_API_PASSWORD = process.env.CPX_METEOALARM_API_PASSWORD
+const MAX_RETRIES = 3
+const TOKEN_EXPIRY_MS = 3600000 // 1 hour in milliseconds
+const API_REQUEST_TIMEOUT_MS = 10000 // 10 seconds
+const DEFAULT_RETRY_DELAY_MULTIPLIER = 1000 // 1 second base delay
+const HTTP_STATUS_OK = 200
+const HTTP_STATUS_CREATED = 201
+const HTTP_STATUS_UNAUTHORIZED = 401
+const config = {
+ retryDelayMultiplier: DEFAULT_RETRY_DELAY_MULTIPLIER // Can be overridden for testing
+}
+
+const getValidToken = async () => {
+ // Check if we have a cached token that hasn't expired
+ if (cachedToken && tokenExpiry && new Date() < tokenExpiry) {
+ return cachedToken
+ }
+
+ try {
+ const response = await axios.post(`${CPX_METEOALARM_API_URL}/tokens`, {
+ username: CPX_METEOALARM_API_USERNAME,
+ password: CPX_METEOALARM_API_PASSWORD
+ }, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (response.status !== HTTP_STATUS_OK) {
+ throw new Error(`Failed to authenticate: ${response.status} ${response.statusText}`)
+ }
+
+ cachedToken = response.data.token
+ // Set token expiry to 1 hour from now
+ tokenExpiry = new Date(Date.now() + TOKEN_EXPIRY_MS)
+ console.log('[meteoalarm] Successfully authenticated and obtained bearer token')
+ return cachedToken
+ } catch (err) {
+ console.error('[meteoalarm] Error fetching bearer token:', err.message)
+ throw new Error(`Failed to authenticate with Meteoalarm: ${err.message}`)
+ }
+}
+
+const postWarning = async (xmlMessage, identifier) => {
+ let lastError = null
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+ try {
+ const token = await getValidToken()
+ const response = await axios.post(`${CPX_METEOALARM_API_URL}/warnings`, xmlMessage, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/xml'
+ },
+ timeout: API_REQUEST_TIMEOUT_MS
+ })
+
+ if (response.status === HTTP_STATUS_CREATED) {
+ console.log(`[meteoalarm] Successfully posted warning: ${identifier}`)
+ console.log('[meteoalarm] Response:', response.data)
+ return response.data
+ }
+ throw new Error(`Received non-201 response: ${response.status}`)
+ } catch (err) {
+ lastError = err
+ console.error(`[meteoalarm] Post attempt ${attempt} failed: ${err.message}`)
+ if (err.response?.data) {
+ console.error('[meteoalarm] Error response:', JSON.stringify(err.response.data))
+ }
+
+ // If it's a 401 error, clear the cached token and retry
+ if (err.response?.status === HTTP_STATUS_UNAUTHORIZED) {
+ console.log('[meteoalarm] Received 401, clearing cached token')
+ cachedToken = null
+ tokenExpiry = null
+ }
+
+ // If this isn't the last attempt, wait before retrying
+ if (attempt < MAX_RETRIES) {
+ const delayMs = attempt * config.retryDelayMultiplier
+ console.log(`[meteoalarm] Waiting ${delayMs}ms before retry...`)
+ await new Promise(resolve => setTimeout(resolve, delayMs))
+ }
+ }
+ }
+ throw new Error(`Failed to post warning to Meteoalarm after ${MAX_RETRIES} attempts: ${lastError.message}`)
+}
+
+const clearTokenCache = () => {
+ cachedToken = null
+ tokenExpiry = null
+}
+
+const setRetryDelayMultiplier = (multiplier) => {
+ config.retryDelayMultiplier = multiplier
+}
+
+module.exports = {
+ postWarning,
+ clearTokenCache,
+ // Export for testing
+ getValidToken,
+ setRetryDelayMultiplier
+}
diff --git a/lib/helpers/redis.js b/lib/helpers/redis.js
index 906afcb..42198a1 100644
--- a/lib/helpers/redis.js
+++ b/lib/helpers/redis.js
@@ -14,11 +14,11 @@ const getClient = () => {
})
client.on('error', (error) => {
- console.error('Redis connection error:', error)
+ console.error('[redis] Connection error:', error)
})
client.on('connect', () => {
- console.log('Redis connected successfully')
+ console.log('[redis] Connected successfully')
})
}
return client
@@ -34,7 +34,7 @@ module.exports = {
try {
return JSON.parse(value)
} catch (error) {
- console.error(`Failed to parse Redis value for key ${key}:`, error)
+ console.error(`[redis] Failed to parse value for key ${key}:`, error)
return value
}
},
diff --git a/lib/models/message.js b/lib/models/message.js
index f3e2b71..e48c903 100644
--- a/lib/models/message.js
+++ b/lib/models/message.js
@@ -89,6 +89,19 @@ class Message {
this.getFirstElement('event').textContent = value
}
+ get responseType () {
+ return this.getFirstElement('responseType')?.textContent || ''
+ }
+
+ set responseType (value) {
+ const responseTypeEl = this.getFirstElement('responseType')
+ if (responseTypeEl) {
+ responseTypeEl.textContent = value
+ } else {
+ this.addElement('event', 'responseType', value)
+ }
+ }
+
get severity () {
return this.getFirstElement('severity')?.textContent || ''
}
@@ -176,6 +189,15 @@ class Message {
}
}
+ removeNode (name) {
+ const nodes = this.doc.getElementsByTagName(name)
+ // Using parentNode.removeChild() because @xmldom/xmldom doesn't support node.remove()
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ const node = nodes[i]
+ node.parentNode.removeChild(node) // NOSONAR - remove() not available in xmldom
+ }
+ }
+
toString () {
return xmlFormat(new xmldom.XMLSerializer().serializeToString(this.doc), { indentation: ' ', collapseContent: true })
}
diff --git a/lib/models/v2MessageMapping.js b/lib/models/v2MessageMapping.js
index 983cc41..5d7d5ad 100644
--- a/lib/models/v2MessageMapping.js
+++ b/lib/models/v2MessageMapping.js
@@ -15,7 +15,7 @@ You should:
- act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan
- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood
-You can also read more about what severe flood warnings are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#severe-flood-warning]
+You can also read more about what severe flood warnings are – https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#severe-flood-warning
Stay up to date
@@ -42,7 +42,7 @@ You should:
- get ready to act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan
- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood
-You can also read more about what flood alerts are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#flood-alert]
+You can also read more about what flood alerts are – https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#flood-alert
Stay up to date
@@ -67,7 +67,7 @@ You should:
- act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan
- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood
-You can also read more about what flood warnings are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#flood-warning]
+You can also read more about what flood warnings are – https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#flood-warning
Stay up to date
diff --git a/package-lock.json b/package-lock.json
index 8fd5bc3..9ceb2e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,11 @@
"version": "3.0.0",
"license": "OGL",
"dependencies": {
- "@aws-sdk/client-sns": "3.981.0",
+ "@aws-sdk/client-sns": "3.995.0",
"@xmldom/xmldom": "0.8.11",
+ "axios": "1.13.5",
"feed": "5.2.0",
- "ioredis": "5.9.2",
+ "ioredis": "5.9.3",
"joi": "18.0.2",
"moment": "2.30.1",
"pg": "8.18.0",
@@ -173,45 +174,45 @@
}
},
"node_modules/@aws-sdk/client-sns": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.981.0.tgz",
- "integrity": "sha512-vz4KUqG7yrjDwFok9B4nQylyNvLb1D+OVywmDzMzLG3t1nIPiSunMMHOK8NltwhUxU+Tp/exPgPUmgt/0arcQA==",
+ "version": "3.995.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.995.0.tgz",
+ "integrity": "sha512-dfkXzxcuz9YXF6JZOChaA18haLDX3aHRPCPUbHoq6hqvTL0TvXLkwPMUv/2LwATXRDB0sceMnpDMZaf5R1YGcQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-node": "^3.972.4",
+ "@aws-sdk/core": "^3.973.11",
+ "@aws-sdk/credential-provider-node": "^3.972.10",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.11",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.981.0",
+ "@aws-sdk/util-endpoints": "3.995.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.10",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.23.2",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.16",
+ "@smithy/middleware-retry": "^4.4.33",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.10",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.32",
+ "@smithy/util-defaults-mode-node": "^4.2.35",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -223,44 +224,44 @@
}
},
"node_modules/@aws-sdk/client-sso": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz",
- "integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==",
+ "version": "3.993.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.993.0.tgz",
+ "integrity": "sha512-VLUN+wIeNX24fg12SCbzTUBnBENlL014yMKZvRhPkcn4wHR6LKgNrjsG3fZ03Xs0XoKaGtNFi1VVrq666sGBoQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.11",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.11",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
+ "@aws-sdk/util-endpoints": "3.993.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.9",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.23.2",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.16",
+ "@smithy/middleware-retry": "^4.4.33",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.10",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.32",
+ "@smithy/util-defaults-mode-node": "^4.2.35",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -272,9 +273,9 @@
}
},
"node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "version": "3.993.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.993.0.tgz",
+ "integrity": "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -288,19 +289,19 @@
}
},
"node_modules/@aws-sdk/core": {
- "version": "3.973.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz",
- "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==",
+ "version": "3.973.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.11.tgz",
+ "integrity": "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/xml-builder": "^3.972.2",
- "@smithy/core": "^3.22.0",
+ "@aws-sdk/xml-builder": "^3.972.5",
+ "@smithy/core": "^3.23.2",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-middleware": "^4.2.8",
@@ -312,12 +313,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz",
- "integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==",
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.9.tgz",
+ "integrity": "sha512-ZptrOwQynfupubvcngLkbdIq/aXvl/czdpEG8XJ8mN8Nb19BR0jaK0bR+tfuMU36Ez9q4xv7GGkHFqEEP2hUUQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.11",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/types": "^4.12.0",
@@ -328,20 +329,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-http": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz",
- "integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==",
+ "version": "3.972.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.11.tgz",
+ "integrity": "sha512-hECWoOoH386bGr89NQc9vA/abkGf5TJrMREt+lhNcnSNmoBS04fK7vc3LrJBSQAUGGVj0Tz3f4dHB3w5veovig==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.11",
"@aws-sdk/types": "^3.973.1",
"@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.10",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.12",
"tslib": "^2.6.2"
},
"engines": {
@@ -349,19 +350,19 @@
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz",
- "integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==",
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.9.tgz",
+ "integrity": "sha512-zr1csEu9n4eDiHMTYJabX1mDGuGLgjgUnNckIivvk43DocJC9/f6DefFrnUPZXE+GHtbW50YuXb+JIxKykU74A==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-env": "^3.972.3",
- "@aws-sdk/credential-provider-http": "^3.972.5",
- "@aws-sdk/credential-provider-login": "^3.972.3",
- "@aws-sdk/credential-provider-process": "^3.972.3",
- "@aws-sdk/credential-provider-sso": "^3.972.3",
- "@aws-sdk/credential-provider-web-identity": "^3.972.3",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.11",
+ "@aws-sdk/credential-provider-env": "^3.972.9",
+ "@aws-sdk/credential-provider-http": "^3.972.11",
+ "@aws-sdk/credential-provider-login": "^3.972.9",
+ "@aws-sdk/credential-provider-process": "^3.972.9",
+ "@aws-sdk/credential-provider-sso": "^3.972.9",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.9",
+ "@aws-sdk/nested-clients": "3.993.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
@@ -374,13 +375,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz",
- "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==",
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.9.tgz",
+ "integrity": "sha512-m4RIpVgZChv0vWS/HKChg1xLgZPpx8Z+ly9Fv7FwA8SOfuC6I3htcSaBz2Ch4bneRIiBUhwP4ziUo0UZgtJStQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.11",
+ "@aws-sdk/nested-clients": "3.993.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
@@ -393,17 +394,17 @@
}
},
"node_modules/@aws-sdk/credential-provider-node": {
- "version": "3.972.4",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz",
- "integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==",
+ "version": "3.972.10",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.10.tgz",
+ "integrity": "sha512-70nCESlvnzjo4LjJ8By8MYIiBogkYPSXl3WmMZfH9RZcB/Nt9qVWbFpYj6Fk1vLa4Vk8qagFVeXgxdieMxG1QA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/credential-provider-env": "^3.972.3",
- "@aws-sdk/credential-provider-http": "^3.972.5",
- "@aws-sdk/credential-provider-ini": "^3.972.3",
- "@aws-sdk/credential-provider-process": "^3.972.3",
- "@aws-sdk/credential-provider-sso": "^3.972.3",
- "@aws-sdk/credential-provider-web-identity": "^3.972.3",
+ "@aws-sdk/credential-provider-env": "^3.972.9",
+ "@aws-sdk/credential-provider-http": "^3.972.11",
+ "@aws-sdk/credential-provider-ini": "^3.972.9",
+ "@aws-sdk/credential-provider-process": "^3.972.9",
+ "@aws-sdk/credential-provider-sso": "^3.972.9",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.9",
"@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
@@ -416,12 +417,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-process": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz",
- "integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==",
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.9.tgz",
+ "integrity": "sha512-gOWl0Fe2gETj5Bk151+LYKpeGi2lBDLNu+NMNpHRlIrKHdBmVun8/AalwMK8ci4uRfG5a3/+zvZBMpuen1SZ0A==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.11",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -433,14 +434,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz",
- "integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==",
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.9.tgz",
+ "integrity": "sha512-ey7S686foGTArvFhi3ifQXmgptKYvLSGE2250BAQceMSXZddz7sUSNERGJT2S7u5KIe/kgugxrt01hntXVln6w==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/client-sso": "3.980.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/token-providers": "3.980.0",
+ "@aws-sdk/client-sso": "3.993.0",
+ "@aws-sdk/core": "^3.973.11",
+ "@aws-sdk/token-providers": "3.993.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -452,13 +453,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz",
- "integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==",
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.9.tgz",
+ "integrity": "sha512-8LnfS76nHXoEc9aRRiMMpxZxJeDG0yusdyo3NvPhCgESmBUgpMa4luhGbClW5NoX/qRcGxxM6Z/esqANSNMTow==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.11",
+ "@aws-sdk/nested-clients": "3.993.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -515,15 +516,15 @@
}
},
"node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz",
- "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==",
+ "version": "3.972.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.11.tgz",
+ "integrity": "sha512-R8CvPsPHXwzIHCAza+bllY6PrctEk4lYq/SkHJz9NLoBHCcKQrbOcsfXxO6xmipSbUNIbNIUhH0lBsJGgsRdiw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.11",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
- "@smithy/core": "^3.22.0",
+ "@aws-sdk/util-endpoints": "3.993.0",
+ "@smithy/core": "^3.23.2",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -533,9 +534,9 @@
}
},
"node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "version": "3.993.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.993.0.tgz",
+ "integrity": "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -549,44 +550,44 @@
}
},
"node_modules/@aws-sdk/nested-clients": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz",
- "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==",
+ "version": "3.993.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.993.0.tgz",
+ "integrity": "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.11",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.11",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
+ "@aws-sdk/util-endpoints": "3.993.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.9",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.23.2",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.16",
+ "@smithy/middleware-retry": "^4.4.33",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.10",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.32",
+ "@smithy/util-defaults-mode-node": "^4.2.35",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -598,9 +599,9 @@
}
},
"node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "version": "3.993.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.993.0.tgz",
+ "integrity": "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -630,13 +631,13 @@
}
},
"node_modules/@aws-sdk/token-providers": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz",
- "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==",
+ "version": "3.993.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.993.0.tgz",
+ "integrity": "sha512-+35g4c+8r7sB9Sjp1KPdM8qxGn6B/shBjJtEUN4e+Edw9UEQlZKIzioOGu3UAbyE0a/s450LdLZr4wbJChtmww==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.11",
+ "@aws-sdk/nested-clients": "3.993.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -661,9 +662,9 @@
}
},
"node_modules/@aws-sdk/util-endpoints": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.981.0.tgz",
- "integrity": "sha512-a8nXh/H3/4j+sxhZk+N3acSDlgwTVSZbX9i55dx41gI1H+geuonuRG+Shv3GZsCb46vzc08RK2qC78ypO8uRlg==",
+ "version": "3.995.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.995.0.tgz",
+ "integrity": "sha512-aym/pjB8SLbo9w2nmkrDdAAVKVlf7CM71B9mKhjDbJTzwpSFBPHqJIMdDyj0mLumKC0aIVDr1H6U+59m9GvMFw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -701,12 +702,12 @@
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz",
- "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==",
+ "version": "3.972.10",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.10.tgz",
+ "integrity": "sha512-LVXzICPlsheET+sE6tkcS47Q5HkSTrANIlqL1iFxGAY/wRQ236DX/PCAK56qMh9QJoXAfXfoRW0B0Og4R+X7Nw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.11",
"@aws-sdk/types": "^3.973.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/types": "^4.12.0",
@@ -725,13 +726,13 @@
}
},
"node_modules/@aws-sdk/xml-builder": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz",
- "integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.5.tgz",
+ "integrity": "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.12.0",
- "fast-xml-parser": "5.3.4",
+ "fast-xml-parser": "5.3.6",
"tslib": "^2.6.2"
},
"engines": {
@@ -804,9 +805,9 @@
}
},
"node_modules/@babel/eslint-parser": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
- "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz",
+ "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1127,9 +1128,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
- "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "version": "9.39.3",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz",
+ "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1597,9 +1598,9 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.22.1",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz",
- "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==",
+ "version": "3.23.3",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.3.tgz",
+ "integrity": "sha512-5IETfbqrTuGs0fC22ZnTW6df+PHlrWpSbAbySzTozsUROWPiOXDIWt1Y4dCDzhJUQ6H3ig/dFOZaEeLsTjNGRQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/middleware-serde": "^4.2.9",
@@ -1608,7 +1609,7 @@
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.11",
+ "@smithy/util-stream": "^4.5.13",
"@smithy/util-utf8": "^4.2.0",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
@@ -1704,12 +1705,12 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.4.13",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz",
- "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==",
+ "version": "4.4.17",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.17.tgz",
+ "integrity": "sha512-QP8wuZ7iSNEQ4/HyihTHlDUlQ3eBrCo+HoMm8l2gPcNrR4TA1RCC10jR7IyCnn3ASTrUwEnRaQ062vFC2/eYJw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.22.1",
+ "@smithy/core": "^3.23.3",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -1723,15 +1724,15 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.4.30",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz",
- "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==",
+ "version": "4.4.34",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.34.tgz",
+ "integrity": "sha512-ROmCX/ev7ryOzgsQ6dnJ46gbVSrvR2HX7ioxkfXlrgfKEMMOUCWgl/OMOi7PZn95CXTxMMNJTbP3nkvWGFTz+w==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/service-error-classification": "^4.2.8",
- "@smithy/smithy-client": "^4.11.2",
+ "@smithy/smithy-client": "^4.11.6",
"@smithy/types": "^4.12.0",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -1785,9 +1786,9 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.4.9",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz",
- "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==",
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz",
+ "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/abort-controller": "^4.2.8",
@@ -1898,17 +1899,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.11.2",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz",
- "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==",
+ "version": "4.11.6",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.6.tgz",
+ "integrity": "sha512-g9FNlCTfQzkSpHW3ILOm+TWZfXuOj2UcrNWNBHLnY3Ch+67mLVmiu3fGWPWbs1XiRK174q5tGphnPCTHvImQUA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.22.1",
- "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/core": "^3.23.3",
+ "@smithy/middleware-endpoint": "^4.4.17",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
- "@smithy/util-stream": "^4.5.11",
+ "@smithy/util-stream": "^4.5.13",
"tslib": "^2.6.2"
},
"engines": {
@@ -2005,13 +2006,13 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.3.29",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz",
- "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==",
+ "version": "4.3.33",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.33.tgz",
+ "integrity": "sha512-VutP/lyBWaTNUzNjI+NC3Kwts4Grhb8CTUyGZNQadf5lujqNy2IIM739D31qplSdbxqYBLOPvMXwy4CIKOArrg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/property-provider": "^4.2.8",
- "@smithy/smithy-client": "^4.11.2",
+ "@smithy/smithy-client": "^4.11.6",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -2020,16 +2021,16 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.2.32",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz",
- "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==",
+ "version": "4.2.36",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.36.tgz",
+ "integrity": "sha512-x73FjvOgG8XBtxu4auMnMDhLi6bUVBLHgNAv8xU0noDGks0KF59JNSzgVQ0oOSuf/D6pVJ5tMEkajwz6IavBUg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/config-resolver": "^4.4.6",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
- "@smithy/smithy-client": "^4.11.2",
+ "@smithy/smithy-client": "^4.11.6",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -2091,13 +2092,13 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.5.11",
- "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz",
- "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==",
+ "version": "4.5.13",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.13.tgz",
+ "integrity": "sha512-ZJQh++mmjO7JiWAW4SdWFrsde1VE038g4uGtkTlvCGcpytMLsxIAg9o9blorLYaQG47EfY9QjLP38od88NLL8w==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/node-http-handler": "^4.4.9",
+ "@smithy/node-http-handler": "^4.4.10",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-buffer-from": "^4.2.0",
@@ -2240,10 +2241,11 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -2467,6 +2469,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2537,6 +2545,17 @@
"node": ">=8"
}
},
+ "node_modules/axios": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2544,9 +2563,9 @@
"dev": true
},
"node_modules/bowser": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
- "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": {
@@ -2657,7 +2676,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -2774,6 +2792,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2914,6 +2944,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -2962,7 +3001,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -3063,7 +3101,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3073,7 +3110,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3111,7 +3147,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -3124,7 +3159,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -3191,9 +3225,9 @@
}
},
"node_modules/eslint": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
- "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "version": "9.39.3",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz",
+ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3203,7 +3237,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.39.2",
+ "@eslint/js": "9.39.3",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -3727,9 +3761,9 @@
"dev": true
},
"node_modules/fast-xml-parser": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
- "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz",
+ "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==",
"funding": [
{
"type": "github",
@@ -3738,7 +3772,7 @@
],
"license": "MIT",
"dependencies": {
- "strnum": "^2.1.0"
+ "strnum": "^2.1.2"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -3848,6 +3882,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -3864,6 +3918,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3875,7 +3945,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -3935,7 +4004,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -3960,7 +4028,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -4089,7 +4156,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4197,7 +4263,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4210,7 +4275,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -4226,7 +4290,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -4304,9 +4367,9 @@
}
},
"node_modules/ioredis": {
- "version": "5.9.2",
- "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz",
- "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz",
+ "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "1.5.0",
@@ -4992,7 +5055,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5030,11 +5092,33 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
+ "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -5737,6 +5821,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/proxyquire": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
diff --git a/package.json b/package.json
index 9dfb8b5..19f1d88 100644
--- a/package.json
+++ b/package.json
@@ -18,10 +18,11 @@
"author": "The Environment Agency",
"license": "OGL",
"dependencies": {
- "@aws-sdk/client-sns": "3.981.0",
+ "@aws-sdk/client-sns": "3.995.0",
"@xmldom/xmldom": "0.8.11",
"feed": "5.2.0",
- "ioredis": "5.9.2",
+ "axios": "1.13.5",
+ "ioredis": "5.9.3",
"joi": "18.0.2",
"moment": "2.30.1",
"pg": "8.18.0",
diff --git a/readme.md b/readme.md
index 1d732cf..2d94eb0 100644
--- a/readme.md
+++ b/readme.md
@@ -20,6 +20,10 @@ This project provides CAP XML services through the use of AWS Lambda.
| CPX_REDIS_HOST | Redis/Elasticache host| yes | | |
| CPX_REDIS_PORT | Redis/Elasticache port| yes | | |
| CPX_REDIS_TLS | Redis/Elasticache tls | yes | | |
+| CPX_METEOALARM_API_URL | Meteoalarm url | yes | | |
+| CPX_METEOALARM_API_USERNAME | Meteoalarm username | yes | | |
+| CPX_METEOALARM_API_PASSWORD | Meteoalarm password | yes | | |
+| CPX_METEOALARM_DISABLE | Meteoalarm toggle | no | | |
## Prerequisites
diff --git a/test/lib/functions/getMessageAtomValidation.js b/test/lib/functions/getMessageAtomValidation.js
index 0137470..36b1cff 100644
--- a/test/lib/functions/getMessageAtomValidation.js
+++ b/test/lib/functions/getMessageAtomValidation.js
@@ -35,16 +35,16 @@ lab.experiment('getMessagesAtom validation logging', () => {
// capture console.log
const logs = []
const originalLog = console.log
- console.log = (msg) => { logs.push(String(msg)) }
+ console.log = (...args) => { logs.push(args.map(a => String(a)).join(' ')) }
try {
const ret = await getMessagesAtom({})
Code.expect(ret.statusCode).to.equal(200)
Code.expect(ret.headers['content-type']).to.equal('application/xml')
- // Banner
- Code.expect(logs[0]).to.equal('ATOM feed failed validation')
- // JSON of the errors
- Code.expect(logs[1]).to.equal(JSON.stringify([{ message: 'bad', line: 12, column: 4 }]))
+ // Check for validation error logs with new prefix format
+ Code.expect(logs.some(l => l.includes('[getMessagesAtom] ATOM feed failed validation'))).to.equal(true)
+ Code.expect(logs.some(l => l.includes('[getMessagesAtom] Validation errors:'))).to.equal(true)
+ Code.expect(logs.some(l => l.includes(JSON.stringify([{ message: 'bad', line: 12, column: 4 }])))).to.equal(true)
} finally {
console.log = originalLog
}
@@ -61,7 +61,7 @@ lab.experiment('getMessagesAtom validation logging', () => {
try {
const ret = await getMessagesAtom({})
Code.expect(ret.statusCode).to.equal(200)
- Code.expect(logs.some(l => l.includes('ATOM feed failed validation'))).to.equal(false)
+ Code.expect(logs.some(l => l.includes('failed validation'))).to.equal(false)
} finally {
console.log = originalLog
}
diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js
index 170cf52..02f8d1a 100644
--- a/test/lib/functions/processMessage.js
+++ b/test/lib/functions/processMessage.js
@@ -11,6 +11,7 @@ const processMessage = require('../../../lib/functions/processMessage').processM
const service = require('../../../lib/helpers/service')
const aws = require('../../../lib/helpers/aws')
const redis = require('../../../lib/helpers/redis')
+const meteoalarm = require('../../../lib/helpers/meteoalarm')
const Message = require('../../../lib/models/message')
const v2MessageMapping = require('../../../lib/models/v2MessageMapping')
const nwsAlert = { bodyXml: fs.readFileSync(path.join(__dirname, 'data', 'nws-alert.xml'), 'utf8') }
@@ -18,10 +19,10 @@ const ORIGINAL_ENV = process.env
let clock
const tomorrow = new Date(new Date().getTime() + (24 * 60 * 60 * 1000))
const identifier = '4eb3b7350ab7aa443650fc9351f02940E'
-const identifierV2 = `2.49.0.0.826.1.20251106080027.${identifier}`
+const identifierV2 = `2.49.0.1.826.1.20251106080027.${identifier}`
const code = 'MCP:v2.0'
const referencesV1 = 'www.gov.uk/environment-agency,4eb3b7350ab7aa443650fc9351f2,2020-01-01T00:00:00+00:00'
-const referencesV2 = 'www.gov.uk/environment-agency,2.49.0.0.826.1.20251106080027.4eb3b7350ab7aa443650fc9351f02940E,2020-01-01T00:00:00+00:00'
+const referencesV2 = 'www.gov.uk/environment-agency,2.49.0.1.826.1.20251106080027.4eb3b7350ab7aa443650fc9351f02940E,2020-01-01T00:00:00+00:00'
// ***********************************************************
// Helper functions
@@ -31,6 +32,7 @@ const expectResponse = (response, putQuery, severity = 'Minor', status = 'Test',
expectMessageV1(new Message(putQuery.values[3]), severity, status, references, previousReferences, quickdialNumber)
expectMessageV2(new Message(putQuery.values[10]), severity, status, references, previousReferences, quickdialNumber)
expectRedisSet(identifier)
+ expectMeteoalarmPost(putQuery.values[10])
}
const expectRedisSet = (identifier) => {
@@ -43,6 +45,13 @@ const expectRedisSet = (identifier) => {
Code.expect(value.alert_v2).to.not.be.empty()
}
+const expectMeteoalarmPost = (messageV2Xml) => {
+ Code.expect(meteoalarm.postWarning.calledOnce).to.be.true()
+ const [xmlMessage, messageIdentifier] = meteoalarm.postWarning.firstCall.args
+ Code.expect(xmlMessage).to.equal(messageV2Xml)
+ Code.expect(messageIdentifier).to.equal(identifier)
+}
+
const expectResponseAndPutQuery = (response, putQuery, status, msgType, references, previousReferences) => {
// test response
Code.expect(response.statusCode).to.equal(200)
@@ -165,6 +174,8 @@ lab.experiment('processMessage', () => {
})
// mock redis
sinon.stub(redis, 'set').resolves('OK')
+ // mock meteoalarm
+ sinon.stub(meteoalarm, 'postWarning').resolves({ id: 'meteoalarm-warning-id' })
})
lab.afterEach(() => {
@@ -184,16 +195,19 @@ lab.experiment('processMessage', () => {
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(nwsAlert)
expectResponse(response, putQuery, 'Minor')
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate')
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe')
})
@@ -210,16 +224,19 @@ lab.experiment('processMessage', () => {
})
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(nwsAlert)
expectResponse(response, putQuery, 'Minor')
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate')
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe')
})
@@ -233,16 +250,19 @@ lab.experiment('processMessage', () => {
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(nwsAlert)
expectResponse(response, putQuery, 'Minor', 'Actual')
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate', 'Actual')
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe', 'Actual')
})
@@ -266,16 +286,19 @@ lab.experiment('processMessage', () => {
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(nwsAlert)
expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true)
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate', 'Test', 'Update', true)
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true)
})
@@ -300,16 +323,19 @@ lab.experiment('processMessage', () => {
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(nwsAlert)
expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true)
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate', 'Actual', 'Update', true)
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true)
})
@@ -338,16 +364,19 @@ lab.experiment('processMessage', () => {
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(alert)
expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true, true, false)
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: alert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate', 'Test', 'Update', true, true, false)
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: alert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true, true, false)
})
@@ -374,16 +403,19 @@ lab.experiment('processMessage', () => {
// do alert and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
let response = await processMessage(nwsAlert)
expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true, true)
// do warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') })
expectResponse(response, putQuery, 'Moderate', 'Actual', 'Update', true, true)
// do severe warning and test output xml
redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') })
expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true, true)
})
@@ -430,4 +462,214 @@ lab.experiment('processMessage', () => {
})
await Code.expect(processMessage(nwsAlert)).to.reject()
})
+
+ lab.test('Throws error when pre/post validation has errors with no SNS message', async () => {
+ const consoleLogStub = sinon.stub(console, 'log')
+ const badAlert = { bodyXml: nwsAlert.bodyXml.replace('4eb3b7350ab7aa443650fc9351f02940E', '') }
+ await Code.expect(processMessage(badAlert)).to.reject()
+ // Check if bodyXml was logged with the new prefix format
+ const bodyXmlLogCall = consoleLogStub.getCalls().find(call =>
+ call.args[0] === '[processMessage] Failed message body:' && call.args[1] === badAlert.bodyXml
+ )
+ Code.expect(bodyXmlLogCall).to.exist()
+ consoleLogStub.restore()
+ })
+
+ lab.test('Throws error when pre/post validation has errors with SNS message sent', async () => {
+ sinon.stub(aws.email, 'publishMessage').resolves()
+ process.env.CPX_SNS_TOPIC = 'arn:aws:sns:region:account:topic'
+ const consoleLogStub = sinon.stub(console, 'log')
+ const badAlert = { bodyXml: nwsAlert.bodyXml.replace('4eb3b7350ab7aa443650fc9351f02940E', '') }
+ const err = await Code.expect(processMessage(badAlert)).to.reject()
+ Code.expect(err.message).to.contain('[500]')
+ Code.expect(aws.email.publishMessage.calledOnce).to.be.true()
+ // Check if bodyXml was logged with the new prefix format
+ const bodyXmlLogCall = consoleLogStub.getCalls().find(call =>
+ call.args[0] === '[processMessage] Failed message body:' && call.args[1] === badAlert.bodyXml
+ )
+ Code.expect(bodyXmlLogCall).to.exist()
+ consoleLogStub.restore()
+ })
+
+ lab.test('does not log when validator has no errors', async () => {
+ const consoleLogStub = sinon.stub(console, 'log')
+ service.putMessage = (query) => Promise.resolve()
+ const response = await processMessage(nwsAlert)
+ Code.expect(response.statusCode).to.equal(200)
+ // Check that the error logging for validation didn't occur
+ // (processMessage itself logs processing messages, so we check it doesn't log the bodyXml with error prefix)
+ const bodyXmlErrorLog = consoleLogStub.getCalls().find(call =>
+ call.args[0] === '[processMessage] Failed message body:'
+ )
+ Code.expect(bodyXmlErrorLog).to.not.exist()
+ consoleLogStub.restore()
+ })
+
+ lab.test('Meteoalarm failure triggers error with no SNS notification (no SNS configured)', async () => {
+ const consoleLogStub = sinon.stub(console, 'log')
+ meteoalarm.postWarning.rejects(new Error('Meteoalarm API unavailable'))
+
+ const putMessageStub = sinon.stub(service, 'putMessage').resolves()
+
+ const err = await Code.expect(processMessage(nwsAlert)).to.reject()
+
+ // Should throw the meteoalarm error
+ Code.expect(err.message).to.equal('Meteoalarm API unavailable')
+
+ // Should have logged the bodyXml with error prefix
+ const bodyXmlLogCall = consoleLogStub.getCalls().find(call =>
+ call.args[0] === '[processMessage] Failed message body:' && call.args[1] === nwsAlert.bodyXml
+ )
+ Code.expect(bodyXmlLogCall).to.exist()
+
+ // Should have attempted other services before meteoalarm failed
+ Code.expect(putMessageStub.calledOnce).to.be.true()
+ Code.expect(redis.set.calledOnce).to.be.true()
+
+ consoleLogStub.restore()
+ })
+
+ lab.test('Meteoalarm failure triggers error with SNS notification', async () => {
+ sinon.stub(aws.email, 'publishMessage').resolves()
+ process.env.CPX_SNS_TOPIC = 'arn:aws:sns:region:account:topic'
+ const consoleLogStub = sinon.stub(console, 'log')
+ meteoalarm.postWarning.rejects(new Error('Meteoalarm API unavailable'))
+
+ const putMessageStub = sinon.stub(service, 'putMessage').resolves()
+
+ const err = await Code.expect(processMessage(nwsAlert)).to.reject()
+
+ // Should throw the error with [500] prefix
+ Code.expect(err.message).to.contain('[500]')
+ Code.expect(err.message).to.contain('Meteoalarm API unavailable')
+
+ // Should have sent SNS notification
+ Code.expect(aws.email.publishMessage.calledOnce).to.be.true()
+ const publishArgs = aws.email.publishMessage.firstCall.args[0]
+ Code.expect(publishArgs.receivedMessage).to.equal(JSON.stringify(nwsAlert.bodyXml))
+ Code.expect(publishArgs.errorMessage).to.equal('Meteoalarm API unavailable')
+ Code.expect(publishArgs.dateCreated).to.exist()
+
+ // Should have logged the bodyXml with error prefix
+ const bodyXmlLogCall = consoleLogStub.getCalls().find(call =>
+ call.args[0] === '[processMessage] Failed message body:' && call.args[1] === nwsAlert.bodyXml
+ )
+ Code.expect(bodyXmlLogCall).to.exist()
+
+ // Should have attempted other services before meteoalarm failed
+ Code.expect(putMessageStub.calledOnce).to.be.true()
+ Code.expect(redis.set.calledOnce).to.be.true()
+
+ consoleLogStub.restore()
+ })
+
+ // ***********************************************************
+ // CPX_METEOALARM_DISABLE tests
+ // ***********************************************************
+ lab.test('Meteoalarm post is disabled when CPX_METEOALARM_DISABLE is "true"', async () => {
+ process.env.CPX_METEOALARM_DISABLE = 'true'
+
+ let putQuery
+ service.putMessage = (query) => Promise.resolve().then(() => {
+ putQuery = query
+ })
+
+ redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
+
+ const response = await processMessage(nwsAlert)
+
+ Code.expect(response.statusCode).to.equal(200)
+ Code.expect(response.body.identifier).to.equal(identifier)
+
+ // Database and Redis should still be called
+ Code.expect(putQuery).to.exist()
+ Code.expect(redis.set.calledOnce).to.be.true()
+
+ // Meteoalarm should NOT be called
+ Code.expect(meteoalarm.postWarning.called).to.be.false()
+ })
+
+ lab.test('Meteoalarm post is enabled when CPX_METEOALARM_DISABLE is undefined', async () => {
+ delete process.env.CPX_METEOALARM_DISABLE
+
+ let putQuery
+ service.putMessage = (query) => Promise.resolve().then(() => {
+ putQuery = query
+ })
+
+ redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
+
+ const response = await processMessage(nwsAlert)
+
+ Code.expect(response.statusCode).to.equal(200)
+ Code.expect(response.body.identifier).to.equal(identifier)
+
+ // All three services should be called
+ Code.expect(putQuery).to.exist()
+ Code.expect(redis.set.calledOnce).to.be.true()
+ Code.expect(meteoalarm.postWarning.calledOnce).to.be.true()
+ })
+
+ lab.test('Meteoalarm post is enabled when CPX_METEOALARM_DISABLE is "false"', async () => {
+ process.env.CPX_METEOALARM_DISABLE = 'false'
+
+ let putQuery
+ service.putMessage = (query) => Promise.resolve().then(() => {
+ putQuery = query
+ })
+
+ redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
+
+ const response = await processMessage(nwsAlert)
+
+ Code.expect(response.statusCode).to.equal(200)
+ Code.expect(response.body.identifier).to.equal(identifier)
+
+ // All three services should be called
+ Code.expect(putQuery).to.exist()
+ Code.expect(redis.set.calledOnce).to.be.true()
+ Code.expect(meteoalarm.postWarning.calledOnce).to.be.true()
+ })
+
+ lab.test('Meteoalarm post is enabled when CPX_METEOALARM_DISABLE is any other value', async () => {
+ process.env.CPX_METEOALARM_DISABLE = 'something-else'
+
+ let putQuery
+ service.putMessage = (query) => Promise.resolve().then(() => {
+ putQuery = query
+ })
+
+ redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
+
+ const response = await processMessage(nwsAlert)
+
+ Code.expect(response.statusCode).to.equal(200)
+ Code.expect(response.body.identifier).to.equal(identifier)
+
+ // All three services should be called
+ Code.expect(putQuery).to.exist()
+ Code.expect(redis.set.calledOnce).to.be.true()
+ Code.expect(meteoalarm.postWarning.calledOnce).to.be.true()
+ })
+
+ lab.test('Meteoalarm post is disabled even when other operations fail if CPX_METEOALARM_DISABLE is "true"', async () => {
+ process.env.CPX_METEOALARM_DISABLE = 'true'
+
+ // Make database fail to ensure meteoalarm is still not called
+ service.putMessage = (query) => Promise.reject(new Error('database error'))
+
+ redis.set.resetHistory()
+ meteoalarm.postWarning.resetHistory()
+
+ const err = await Code.expect(processMessage(nwsAlert)).to.reject()
+
+ Code.expect(err.message).to.equal('database error')
+
+ // Meteoalarm should still NOT be called even though other operations were attempted
+ Code.expect(meteoalarm.postWarning.called).to.be.false()
+ })
})
diff --git a/test/lib/functions/processMessageValidation.js b/test/lib/functions/processMessageValidation.js
index 828b3d6..2a8d18d 100644
--- a/test/lib/functions/processMessageValidation.js
+++ b/test/lib/functions/processMessageValidation.js
@@ -17,6 +17,7 @@ const fakeService = {
const fakeSchema = { validateAsync: async () => ({ error: null }) }
const fakeAws = { email: { publishMessage: sinon.stub() } }
const fakeRedis = { set: sinon.stub().resolves('OK') }
+const fakeMeteoalarm = { postWarning: sinon.stub().resolves({ id: 'meteoalarm-warning-id' }) }
const loadWithValidateMock = (validateMock) => {
return Proxyquire('../../../lib/functions/processMessage', {
@@ -24,7 +25,8 @@ const loadWithValidateMock = (validateMock) => {
'../helpers/service': fakeService,
'../schemas/processMessageEventSchema': fakeSchema,
'../helpers/aws': fakeAws,
- '../helpers/redis': fakeRedis
+ '../helpers/redis': fakeRedis,
+ '../helpers/meteoalarm': fakeMeteoalarm
}).processMessage
}
@@ -33,8 +35,12 @@ const CPX_SNS_TOPIC = process.env.CPX_SNS_TOPIC
lab.experiment('processMessage validation logging', () => {
lab.afterEach(() => {
process.env.CPX_SNS_TOPIC = CPX_SNS_TOPIC
+ fakeAws.email.publishMessage.resetHistory()
+ fakeRedis.set.resetHistory()
+ fakeMeteoalarm.postWarning.resetHistory()
})
lab.test('Throws error when pre/post validation has errors with no SNS message', async () => {
+ delete process.env.CPX_SNS_TOPIC
const validateMock = async () => ({ errors: [{ message: 'oops' }] })
const processMessage = loadWithValidateMock(validateMock)
@@ -66,7 +72,7 @@ lab.experiment('processMessage validation logging', () => {
try {
await processMessage(nwsAlert)
- Code.expect(logs).to.include('Finished processing CAP message: 4eb3b7350ab7aa443650fc9351f02940E for TESTAREA1')
+ Code.expect(logs.some(l => l.includes('Finished processing CAP message: 4eb3b7350ab7aa443650fc9351f02940E for TESTAREA1'))).to.be.true()
Code.expect(logs.some(l => l.includes('failed validation'))).to.be.false()
} finally {
console.log = origLog
@@ -77,11 +83,13 @@ lab.experiment('processMessage validation logging', () => {
process.env.CPX_SNS_TOPIC = true
const awsStub = { email: { publishMessage: sinon.stub() } }
const redisStub = { set: sinon.stub().resolves('OK') }
+ const meteoalarmStub = { postWarning: sinon.stub().resolves({ id: 'meteoalarm-warning-id' }) }
const processMessage = Proxyquire('../../../lib/functions/processMessage', {
'../helpers/service': fakeService,
'../schemas/processMessageEventSchema': fakeSchema,
'../helpers/aws': awsStub,
- '../helpers/redis': redisStub
+ '../helpers/redis': redisStub,
+ '../helpers/meteoalarm': meteoalarmStub
}).processMessage
const ret = await processMessage(nwsAlert)
diff --git a/test/lib/helpers/message.js b/test/lib/helpers/message.js
index 9ac0e05..ae0f8d4 100644
--- a/test/lib/helpers/message.js
+++ b/test/lib/helpers/message.js
@@ -137,9 +137,10 @@ lab.experiment('getMessage helper', () => {
}]
})
await getMessage(event, false)
- Code.expect(consoleLogStub.callCount).to.equal(2)
- Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('CAP get message failed validation')
- Code.expect(consoleLogStub.getCall(1).args[0]).to.equal('[{"rawMessage":"message.xml:19: Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","message":"Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","loc":{"fileName":"message.xml","lineNumber":19}}]')
+ // Check that validation error logs are present (among other logs)
+ const allLogs = consoleLogStub.getCalls().map(call => call.args[0])
+ Code.expect(allLogs.some(log => log === '[getMessage] CAP get message failed validation')).to.be.true()
+ Code.expect(allLogs.some(log => log === '[getMessage] Validation errors:' || (typeof log === 'string' && log.includes('Schemas validity error')))).to.be.true()
} finally {
consoleLogStub.restore()
}
@@ -156,7 +157,9 @@ lab.experiment('getMessage helper', () => {
}]
})
await getMessage(event, false)
- Code.expect(consoleLogStub.callCount).to.equal(0)
+ // Check that no validation error logs are present
+ const allLogs = consoleLogStub.getCalls().map(call => call.args[0])
+ Code.expect(allLogs.some(log => typeof log === 'string' && log.includes('failed validation'))).to.be.false()
} finally {
consoleLogStub.restore()
}
@@ -271,9 +274,10 @@ lab.experiment('getMessage helper', () => {
}]
})
await getMessage(event, true)
- Code.expect(consoleLogStub.callCount).to.equal(2)
- Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('CAP get message failed validation')
- Code.expect(consoleLogStub.getCall(1).args[0]).to.equal('[{"rawMessage":"message.xml:19: Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","message":"Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","loc":{"fileName":"message.xml","lineNumber":19}}]')
+ // Check that validation error logs are present (among other logs)
+ const allLogs = consoleLogStub.getCalls().map(call => call.args[0])
+ Code.expect(allLogs.some(log => log === '[getMessage] CAP get message failed validation')).to.be.true()
+ Code.expect(allLogs.some(log => log === '[getMessage] Validation errors:' || (typeof log === 'string' && log.includes('Schemas validity error')))).to.be.true()
} finally {
consoleLogStub.restore()
}
@@ -290,7 +294,9 @@ lab.experiment('getMessage helper', () => {
}]
})
await getMessage(event, true)
- Code.expect(consoleLogStub.callCount).to.equal(0)
+ // Check that no validation error logs are present
+ const allLogs = consoleLogStub.getCalls().map(call => call.args[0])
+ Code.expect(allLogs.some(log => typeof log === 'string' && log.includes('failed validation'))).to.be.false()
} finally {
consoleLogStub.restore()
}
@@ -333,8 +339,11 @@ lab.experiment('getMessage helper', () => {
try {
await getMessage(event, false)
} catch (err) {
- Code.expect(consoleLogStub.callCount).to.equal(1)
- Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('No message found for 4eb3b7350ab7aa443650fc9351f')
+ // Find the specific log message among all the logs
+ const noMessageLog = consoleLogStub.getCalls().find(call =>
+ call.args[0] === '[getMessage] No message found for 4eb3b7350ab7aa443650fc9351f'
+ )
+ Code.expect(noMessageLog).to.exist()
} finally {
consoleLogStub.restore()
}
diff --git a/test/lib/helpers/messages.js b/test/lib/helpers/messages.js
index 15b0761..fbac803 100644
--- a/test/lib/helpers/messages.js
+++ b/test/lib/helpers/messages.js
@@ -123,7 +123,7 @@ lab.experiment('messages helper', () => {
alert: 'test',
sent: new Date(),
identifier: '4eb3b7350ab7aa443650fc9351f',
- identifier_v2: '2.49.0.0.826.1.YYYYMMDDHHMMSS.4eb3b7350ab7aa443650fc9351f'
+ identifier_v2: '2.49.0.1.826.1.YYYYMMDDHHMMSS.4eb3b7350ab7aa443650fc9351f'
}]
})
})
@@ -189,14 +189,14 @@ lab.experiment('messages helper', () => {
alert: 'test1',
sent: new Date('2025-01-01'),
identifier: 'id1',
- identifier_v2: '2.49.0.0.826.1.20250101000000.id1'
+ identifier_v2: '2.49.0.1.826.1.20250101000000.id1'
},
{
fwis_code: 'AREA2',
alert: 'test2',
sent: new Date('2025-01-02'),
identifier: 'id2',
- identifier_v2: '2.49.0.0.826.1.20250102000000.id2'
+ identifier_v2: '2.49.0.1.826.1.20250102000000.id2'
}
]
})
@@ -218,7 +218,7 @@ lab.experiment('messages helper', () => {
alert: 'test',
sent: new Date('2025-01-01T12:00:00Z'),
identifier: 'test_id',
- identifier_v2: '2.49.0.0.826.1.20250101120000.test_id'
+ identifier_v2: '2.49.0.1.826.1.20250101120000.test_id'
}]
})
}
@@ -263,7 +263,7 @@ lab.experiment('messages helper', () => {
alert: `test${i}`,
sent: new Date(`2025-01-0${i + 1}`),
identifier: `id${i}`,
- identifier_v2: `2.49.0.0.826.1.2025010${i + 1}000000.id${i}`
+ identifier_v2: `2.49.0.1.826.1.2025010${i + 1}000000.id${i}`
}))
})
}
diff --git a/test/lib/helpers/meteoalarm.js b/test/lib/helpers/meteoalarm.js
new file mode 100644
index 0000000..925aa3e
--- /dev/null
+++ b/test/lib/helpers/meteoalarm.js
@@ -0,0 +1,327 @@
+'use strict'
+
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+const sinon = require('sinon')
+const Proxyquire = require('proxyquire')
+
+const lab = exports.lab = Lab.script()
+
+const ORIGINAL_ENV = process.env
+
+lab.experiment('meteoalarm helper', () => {
+ let meteoalarm
+ let axiosStub
+
+ lab.beforeEach(() => {
+ // Mock environment - must be set before loading module
+ process.env = { ...ORIGINAL_ENV }
+ process.env.CPX_METEOALARM_API_URL = 'https://test-meteoalarm.example.com'
+ process.env.CPX_METEOALARM_API_USERNAME = 'test-user'
+ process.env.CPX_METEOALARM_API_PASSWORD = 'test-password'
+
+ // Create axios stub
+ axiosStub = {
+ post: sinon.stub()
+ }
+
+ // Clear the require cache to force fresh module load
+ delete require.cache[require.resolve('../../../lib/helpers/meteoalarm')]
+
+ // Load module with mocked axios
+ meteoalarm = Proxyquire('../../../lib/helpers/meteoalarm', {
+ axios: axiosStub
+ })
+
+ // Clear any cached token before each test
+ meteoalarm.clearTokenCache()
+ })
+
+ lab.afterEach(() => {
+ sinon.restore()
+ process.env = ORIGINAL_ENV
+ })
+
+ lab.experiment('getValidToken', () => {
+ lab.test('fetches a new token successfully', async () => {
+ axiosStub.post.resolves({
+ status: 200,
+ data: { token: 'test-bearer-token-123' }
+ })
+
+ const token = await meteoalarm.getValidToken()
+
+ Code.expect(token).to.equal('test-bearer-token-123')
+ Code.expect(axiosStub.post.calledOnce).to.be.true()
+ Code.expect(axiosStub.post.firstCall.args[0]).to.equal('https://test-meteoalarm.example.com/tokens')
+ Code.expect(axiosStub.post.firstCall.args[1]).to.equal({
+ username: 'test-user',
+ password: 'test-password'
+ })
+ Code.expect(axiosStub.post.firstCall.args[2].headers['Content-Type']).to.equal('application/json')
+ })
+
+ lab.test('returns cached token if still valid', async () => {
+ // First call to get token
+ axiosStub.post.resolves({
+ status: 200,
+ data: { token: 'cached-token' }
+ })
+
+ const token1 = await meteoalarm.getValidToken()
+ Code.expect(token1).to.equal('cached-token')
+ Code.expect(axiosStub.post.calledOnce).to.be.true()
+
+ // Second call should return cached token without making another API call
+ const token2 = await meteoalarm.getValidToken()
+ Code.expect(token2).to.equal('cached-token')
+ Code.expect(axiosStub.post.calledOnce).to.be.true() // Still only called once
+ })
+
+ lab.test('throws error when authentication fails with non-200 status', async () => {
+ axiosStub.post.resolves({
+ status: 401,
+ statusText: 'Unauthorized'
+ })
+
+ await Code.expect(meteoalarm.getValidToken()).to.reject(Error, 'Failed to authenticate with Meteoalarm: Failed to authenticate: 401 Unauthorized')
+ })
+
+ lab.test('throws error when axios request fails', async () => {
+ axiosStub.post.rejects(new Error('Network error'))
+
+ await Code.expect(meteoalarm.getValidToken()).to.reject(Error, 'Failed to authenticate with Meteoalarm: Network error')
+ })
+ })
+
+ lab.experiment('postWarning', () => {
+ lab.beforeEach(() => {
+ // Set a very short retry delay for testing (10ms instead of 1000ms)
+ meteoalarm.setRetryDelayMultiplier(10)
+ })
+
+ lab.afterEach(() => {
+ // Reset to default
+ meteoalarm.setRetryDelayMultiplier(1000)
+ })
+
+ lab.test('successfully posts warning on first attempt', async () => {
+ const xmlMessage = 'test-id'
+ const identifier = 'test-id'
+
+ // Mock token request
+ axiosStub.post.onFirstCall().resolves({
+ status: 200,
+ data: { token: 'test-token' }
+ })
+
+ // Mock warning post
+ axiosStub.post.onSecondCall().resolves({
+ status: 201,
+ data: { id: 'warning-123', status: 'created' }
+ })
+
+ const result = await meteoalarm.postWarning(xmlMessage, identifier)
+
+ Code.expect(result).to.equal({ id: 'warning-123', status: 'created' })
+ Code.expect(axiosStub.post.calledTwice).to.be.true()
+
+ // Verify warning post call
+ const warningCall = axiosStub.post.secondCall
+ Code.expect(warningCall.args[0]).to.equal('https://test-meteoalarm.example.com/warnings')
+ Code.expect(warningCall.args[1]).to.equal(xmlMessage)
+ Code.expect(warningCall.args[2].headers.Authorization).to.equal('Bearer test-token')
+ Code.expect(warningCall.args[2].headers['Content-Type']).to.equal('application/xml')
+ Code.expect(warningCall.args[2].timeout).to.equal(10000)
+ })
+
+ lab.test('retries on failure and succeeds on second attempt', async () => {
+ const xmlMessage = 'test-id'
+ const identifier = 'test-id'
+
+ // Mock token request using withArgs for better matching
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({
+ status: 200,
+ data: { token: 'test-token' }
+ })
+
+ // Track warning post attempts
+ let attemptCount = 0
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').callsFake(() => {
+ attemptCount++
+ if (attemptCount === 1) {
+ const error = new Error('Timeout')
+ error.response = { data: { error: 'timeout' } }
+ return Promise.reject(error)
+ } else {
+ return Promise.resolve({
+ status: 201,
+ data: { id: 'warning-123' }
+ })
+ }
+ })
+
+ const result = await meteoalarm.postWarning(xmlMessage, identifier)
+
+ Code.expect(result).to.equal({ id: 'warning-123' })
+ Code.expect(attemptCount).to.equal(2)
+ })
+
+ lab.test('clears cached token on 401 and retries', async () => {
+ const xmlMessage = 'test-id'
+ const identifier = 'test-id'
+
+ // Track token requests
+ let tokenRequestCount = 0
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').callsFake(() => {
+ tokenRequestCount++
+ return Promise.resolve({
+ status: 200,
+ data: { token: tokenRequestCount === 1 ? 'expired-token' : 'fresh-token' }
+ })
+ })
+
+ // Track warning post attempts
+ let warningAttemptCount = 0
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').callsFake(() => {
+ warningAttemptCount++
+ if (warningAttemptCount === 1) {
+ const error = new Error('Unauthorized')
+ error.response = { status: 401, data: { error: 'token expired' } }
+ return Promise.reject(error)
+ } else {
+ return Promise.resolve({
+ status: 201,
+ data: { id: 'warning-456' }
+ })
+ }
+ })
+
+ const result = await meteoalarm.postWarning(xmlMessage, identifier)
+
+ Code.expect(result).to.equal({ id: 'warning-456' })
+ // Should have fetched token twice because of 401
+ Code.expect(tokenRequestCount).to.equal(2)
+ })
+
+ lab.test('throws error after max retries exceeded', async () => {
+ const xmlMessage = 'test-id'
+ const identifier = 'test-id'
+
+ // Mock token request
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({
+ status: 200,
+ data: { token: 'test-token' }
+ })
+
+ // All warning posts fail
+ const error = new Error('Service unavailable')
+ error.response = { data: { error: 'service down' } }
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').rejects(error)
+
+ await Code.expect(meteoalarm.postWarning(xmlMessage, identifier)).to.reject(Error, 'Failed to post warning to Meteoalarm after 3 attempts: Service unavailable')
+ })
+
+ lab.test('throws error when non-201 status is received', async () => {
+ const xmlMessage = 'test-id'
+ const identifier = 'test-id'
+
+ // Mock token request
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({
+ status: 200,
+ data: { token: 'test-token' }
+ })
+
+ // Warning post returns non-201 status and will retry
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').resolves({
+ status: 200,
+ data: { message: 'accepted but not created' }
+ })
+
+ await Code.expect(meteoalarm.postWarning(xmlMessage, identifier)).to.reject(Error, 'Failed to post warning to Meteoalarm after 3 attempts: Received non-201 response: 200')
+ })
+
+ lab.test('handles error without response object', async () => {
+ const xmlMessage = 'test-id'
+ const identifier = 'test-id'
+
+ // Mock token request
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({
+ status: 200,
+ data: { token: 'test-token' }
+ })
+
+ // All warning posts fail without response object
+ axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').rejects(new Error('Network error'))
+
+ await Code.expect(meteoalarm.postWarning(xmlMessage, identifier)).to.reject(Error, 'Failed to post warning to Meteoalarm after 3 attempts: Network error')
+ })
+ })
+
+ lab.experiment('clearTokenCache', () => {
+ lab.test('clears cached token requiring new fetch', async () => {
+ // First call to get token
+ axiosStub.post.resolves({
+ status: 200,
+ data: { token: 'first-token' }
+ })
+
+ const token1 = await meteoalarm.getValidToken()
+ Code.expect(token1).to.equal('first-token')
+ Code.expect(axiosStub.post.calledOnce).to.be.true()
+
+ // Clear the cache
+ meteoalarm.clearTokenCache()
+
+ // Mock a different token for next call
+ axiosStub.post.resolves({
+ status: 200,
+ data: { token: 'second-token' }
+ })
+
+ // Next call should fetch a new token
+ const token2 = await meteoalarm.getValidToken()
+ Code.expect(token2).to.equal('second-token')
+ Code.expect(axiosStub.post.calledTwice).to.be.true()
+ })
+ })
+
+ lab.experiment('integration scenarios', () => {
+ lab.test('uses cached token across multiple warning posts', async () => {
+ const xmlMessage1 = 'test-id-1'
+ const xmlMessage2 = 'test-id-2'
+
+ // Mock token request once - should only be called once
+ let tokenCallCount = 0
+ axiosStub.post.callsFake((url, data, config) => {
+ if (url.includes('/tokens')) {
+ tokenCallCount++
+ return Promise.resolve({
+ status: 200,
+ data: { token: 'shared-token' }
+ })
+ } else if (url.includes('/warnings')) {
+ // Return different results for different messages
+ if (data === xmlMessage1) {
+ return Promise.resolve({
+ status: 201,
+ data: { id: 'warning-1' }
+ })
+ } else if (data === xmlMessage2) {
+ return Promise.resolve({
+ status: 201,
+ data: { id: 'warning-2' }
+ })
+ }
+ }
+ return Promise.reject(new Error('Unexpected call'))
+ })
+
+ await meteoalarm.postWarning(xmlMessage1, 'test-id-1')
+ await meteoalarm.postWarning(xmlMessage2, 'test-id-2')
+
+ // Token should only be fetched once
+ Code.expect(tokenCallCount).to.equal(1)
+ })
+ })
+})
diff --git a/test/lib/helpers/redis.js b/test/lib/helpers/redis.js
index f3d95ab..698725e 100644
--- a/test/lib/helpers/redis.js
+++ b/test/lib/helpers/redis.js
@@ -198,7 +198,7 @@ lab.experiment('redis helper', () => {
const mockError = new Error('Connection failed')
errorHandler.args[1](mockError)
- Code.expect(consoleErrorStub.calledWith('Redis connection error:', mockError)).to.be.true()
+ Code.expect(consoleErrorStub.calledWith('[redis] Connection error:', mockError)).to.be.true()
consoleErrorStub.restore()
})
@@ -212,7 +212,7 @@ lab.experiment('redis helper', () => {
const connectHandler = mockRedisInstance.on.getCalls().find(call => call.args[0] === 'connect')
connectHandler.args[1]()
- Code.expect(consoleLogStub.calledWith('Redis connected successfully')).to.be.true()
+ Code.expect(consoleLogStub.calledWith('[redis] Connected successfully')).to.be.true()
consoleLogStub.restore()
})
diff --git a/test/lib/models/message.js b/test/lib/models/message.js
index e881793..8307019 100644
--- a/test/lib/models/message.js
+++ b/test/lib/models/message.js
@@ -181,6 +181,30 @@ lab.experiment('Message class', () => {
Code.expect(message.toString()).to.include('REF2')
})
+ lab.test('responseType defaults to empty string when missing', () => {
+ Code.expect(message.responseType).to.equal('')
+ })
+
+ lab.test('setting responseType adds element when not present', () => {
+ message.responseType = 'Prepare'
+ Code.expect(message.responseType).to.equal('Prepare')
+ Code.expect(message.toString()).to.include('Prepare')
+ })
+
+ lab.test('setting responseType updates existing element', () => {
+ // First set to add the element
+ message.responseType = 'Monitor'
+ Code.expect(message.responseType).to.equal('Monitor')
+
+ // Second set should update existing element
+ message.responseType = 'Evacuate'
+ Code.expect(message.responseType).to.equal('Evacuate')
+ Code.expect(message.toString()).to.include('Evacuate')
+ // Should only have one responseType element
+ const responseTypeCount = (message.toString().match(//g) || []).length
+ Code.expect(responseTypeCount).to.equal(1)
+ })
+
lab.test('parses quickdial number from instruction', () => {
Code.expect(message.quickdialNumber).to.equal('210010')
})
@@ -239,6 +263,7 @@ lab.experiment('Message class', () => {
Code.expect(messageBlank.sent).to.equal('')
Code.expect(messageBlank.code).to.equal('')
Code.expect(messageBlank.event).to.equal('')
+ Code.expect(messageBlank.responseType).to.equal('')
Code.expect(messageBlank.severity).to.equal('')
Code.expect(messageBlank.onset).to.equal('')
Code.expect(messageBlank.headline).to.equal('')
@@ -253,6 +278,7 @@ lab.experiment('Message class', () => {
messageBlank.status = 'Actual'
messageBlank.code = 'CODE123'
messageBlank.event = 'Test Event'
+ messageBlank.responseType = 'Shelter'
messageBlank.severity = 'Severe'
messageBlank.onset = '2026-06-01T10:00:00-00:00'
messageBlank.headline = 'Test Headline'
@@ -264,6 +290,7 @@ lab.experiment('Message class', () => {
Code.expect(messageBlank.status).to.equal('Actual')
Code.expect(messageBlank.code).to.equal('CODE123')
Code.expect(messageBlank.event).to.equal('Test Event')
+ Code.expect(messageBlank.responseType).to.equal('Shelter')
Code.expect(messageBlank.severity).to.equal('Severe')
Code.expect(messageBlank.onset).to.equal('2026-06-01T10:00:00-00:00')
Code.expect(messageBlank.headline).to.equal('Test Headline')
@@ -277,6 +304,7 @@ lab.experiment('Message class', () => {
messageBlank.status = 'Actual'
messageBlank.code = 'CODE123'
messageBlank.event = 'Test Event'
+ messageBlank.responseType = 'AllClear'
messageBlank.severity = 'Severe'
messageBlank.onset = '2026-06-01T10:00:00-00:00'
messageBlank.headline = 'Test Headline'
@@ -288,6 +316,7 @@ lab.experiment('Message class', () => {
Code.expect(messageBlank.status).to.equal('Actual')
Code.expect(messageBlank.code).to.equal('CODE123')
Code.expect(messageBlank.event).to.equal('Test Event')
+ Code.expect(messageBlank.responseType).to.equal('AllClear')
Code.expect(messageBlank.severity).to.equal('Severe')
Code.expect(messageBlank.onset).to.equal('2026-06-01T10:00:00-00:00')
Code.expect(messageBlank.headline).to.equal('Test Headline')
@@ -324,4 +353,83 @@ lab.experiment('Message class', () => {
fwisCode
`))
})
+
+ lab.test('removeNode removes a single node from the document', () => {
+ // Verify instruction exists before removal
+ Code.expect(message.instruction).to.not.be.empty()
+ Code.expect(message.toString()).to.include('')
+
+ // Remove instruction node
+ message.removeNode('instruction')
+
+ // Verify instruction is removed
+ Code.expect(message.instruction).to.equal('')
+ Code.expect(message.toString()).to.not.include('')
+ })
+
+ lab.test('removeNode removes multiple nodes of the same type', () => {
+ // Add multiple parameters
+ message.addParameter('param1', 'value1')
+ message.addParameter('param2', 'value2')
+ message.addParameter('param3', 'value3')
+
+ // Verify parameters exist
+ const xmlBefore = message.toString()
+ Code.expect(xmlBefore).to.include('')
+ Code.expect(xmlBefore).to.include('param1')
+ Code.expect(xmlBefore).to.include('param2')
+ Code.expect(xmlBefore).to.include('param3')
+
+ // Remove all parameter nodes
+ message.removeNode('parameter')
+
+ // Verify all parameters are removed
+ const xmlAfter = message.toString()
+ Code.expect(xmlAfter).to.not.include('')
+ Code.expect(xmlAfter).to.not.include('param1')
+ Code.expect(xmlAfter).to.not.include('param2')
+ Code.expect(xmlAfter).to.not.include('param3')
+ })
+
+ lab.test('removeNode handles non-existent nodes gracefully', () => {
+ const xmlBefore = message.toString()
+
+ // Try to remove a node that doesn't exist
+ message.removeNode('nonExistentNode')
+
+ // XML should remain unchanged
+ const xmlAfter = message.toString()
+ Code.expect(xmlAfter).to.equal(xmlBefore)
+ })
+
+ lab.test('removeNode removes code node when present', () => {
+ const messageWithCode = new Message(blankXml2)
+ messageWithCode.code = 'TEST_CODE'
+
+ // Verify code exists
+ Code.expect(messageWithCode.code).to.equal('TEST_CODE')
+ Code.expect(messageWithCode.toString()).to.include('TEST_CODE')
+
+ // Remove code node
+ messageWithCode.removeNode('code')
+
+ // Verify code is removed
+ Code.expect(messageWithCode.code).to.equal('')
+ Code.expect(messageWithCode.toString()).to.not.include('TEST_CODE')
+ })
+
+ lab.test('removeNode removes references node', () => {
+ message.references = 'REF123'
+
+ // Verify references exists
+ Code.expect(message.references).to.equal('REF123')
+ Code.expect(message.toString()).to.include('REF123')
+
+ // Remove references node
+ message.removeNode('references')
+
+ // Verify references is removed
+ Code.expect(message.references).to.equal('')
+ Code.expect(message.toString()).to.not.include('')
+ })
})