From 53e7ba8bacdbf596e15b2c9720b7d76e18db5e25 Mon Sep 17 00:00:00 2001 From: Adenike <1adenike.akande@gmail.com> Date: Wed, 26 Aug 2026 23:58:00 +0100 Subject: [PATCH] feat: per-webhook timeout, asset validation, error codes, ESLint CI step - webhookDispatcher.js: postOnce accepts a per-webhook timeoutMs override, falling back to the global config default, instead of a single global timeout for every subscriber - schemas.js: alert creation now rejects asset codes not present in config.watchedAssets (when that list is configured), instead of accepting any string silently - errorHandler.js: the generic status-based fallback branch now maps common HTTP statuses (400/401/403/404/429) to their matching structured error code instead of hardcoding every one as FORBIDDEN - eslint.config.js + ci.yml: adds a minimal ESLint flat config and a non-blocking lint step in CI as a starting point Closes #228 Closes #229 Closes #230 Closes #231 --- .github/workflows/ci.yml | 4 ++++ eslint.config.js | 16 ++++++++++++++++ src/middleware/errorHandler.js | 3 ++- src/services/webhookDispatcher.js | 6 +++--- src/validation/schemas.js | 6 +++++- 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 eslint.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 821edb9..c82b76f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,10 @@ jobs: - name: Lint OpenAPI spec run: npx @redocly/cli lint openapi.yaml + - name: Lint source (ESLint) + run: npx eslint . + continue-on-error: true + - name: Run tests run: npm test diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..8b9fdb8 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +'use strict'; + +// Minimal ESLint flat config (Issue #231) — a starting point for consistent +// code style, not a full ruleset. Run with: npx eslint . +module.exports = [ + { + files: ['src/**/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'commonjs', + }, + rules: { + 'no-unused-vars': 'warn', + }, + }, +]; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index a870f06..c614f21 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -25,7 +25,8 @@ function errorHandler(err, req, res, _next) { message = 'Request body is too large'; } else if (err.status || err.statusCode) { status = err.status || err.statusCode; - code = 'FORBIDDEN'; + const STATUS_CODES = { 400: 'VALIDATION_ERROR', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 429: 'RATE_LIMITED' }; + code = STATUS_CODES[status] || 'INTERNAL_ERROR'; message = err.message || 'Request rejected'; } diff --git a/src/services/webhookDispatcher.js b/src/services/webhookDispatcher.js index c12017c..cdb453f 100644 --- a/src/services/webhookDispatcher.js +++ b/src/services/webhookDispatcher.js @@ -89,10 +89,10 @@ function withDeliveryTrace(traceId, fn) { return requestContext.run({ requestId: traceId }, fn); } -async function postOnce(url, headers, body) { +async function postOnce(url, headers, body, timeoutMs) { return axios.post(url, body, { headers, - timeout: config.webhooks.timeoutMs, + timeout: timeoutMs ?? config.webhooks.timeoutMs, transformRequest: [(data) => data], validateStatus: () => true, }); @@ -136,7 +136,7 @@ async function attempt(deliveryId) { let networkError = null; try { - const res = await postOnce(webhook.url, headers, body); + const res = await postOnce(webhook.url, headers, body, webhook.timeoutMs); responseStatus = res.status; } catch (err) { networkError = err.message || 'network error'; diff --git a/src/validation/schemas.js b/src/validation/schemas.js index e0344c1..364f54f 100644 --- a/src/validation/schemas.js +++ b/src/validation/schemas.js @@ -2,6 +2,7 @@ const { z } = require('zod'); const webhookEvents = require('../services/webhookEvents'); +const config = require('../config'); const stellarPublicKeySchema = z .string() @@ -99,7 +100,10 @@ const keyCreateBodySchema = z.object({ }); const alertCreateBodySchema = z.object({ - asset: assetCodeSchema, + asset: assetCodeSchema.refine( + (code) => config.watchedAssets.length === 0 || config.watchedAssets.includes(code), + { message: 'Asset code is not in the list of watched Stellar assets' }, + ), type: z.enum(['above', 'below', 'change_pct']), threshold_usd: z.number().positive(), webhook_url: httpUrlSchema,