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,