@mojaloop/central-services-shared 18.36.0-snapshot.3 → 18.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,7 +20,7 @@ version: 2.1
20
20
  setup: true
21
21
 
22
22
  orbs:
23
- build: mojaloop/build@1.1.9
23
+ build: mojaloop/build@1.1.15
24
24
 
25
25
  workflows:
26
26
  setup:
package/.grype.yaml CHANGED
@@ -1,40 +1,66 @@
1
- # Grype vulnerability scanning configuration for central-services-shared
2
- # This is a library project without Docker images, so we use source scanning
3
1
  scan-type: source
4
-
5
- # Enable vulnerability scanning
6
2
  disabled: false
7
-
8
- # Vulnerability ignore rules
9
- # Add specific CVEs here if they are false positives or acceptable risks
10
3
  ignore:
11
- # Example format for ignoring specific vulnerabilities:
12
- # - vulnerability: "CVE-2023-xxxxx"
13
- # reason: "False positive in dev dependency that doesn't affect production"
14
- # - vulnerability: "GHSA-xxxx-xxxx-xxxx"
15
- # package:
16
- # name: "package-name"
17
- # version: "1.0.0"
18
- # reason: "Not exploitable in our usage context"
19
-
20
- # Output formats for scan results
4
+ - vulnerability: GHSA-2g4f-4pwh-qvx6
5
+ include-aliases: true
6
+ reason: "Unfixable npm transitive vulnerability: ajv ReDoS (moderate) as of 2026-02-19"
7
+ - vulnerability: GHSA-3ppc-4f35-3m26
8
+ include-aliases: true
9
+ reason: "Unfixable npm transitive vulnerability: minimatch ReDoS - fix requires v10 major version break as of 2026-02-19"
10
+ - vulnerability: GHSA-2w6w-674q-4c4q
11
+ include-aliases: true
12
+ reason: "Unfixable npm transitive vulnerability: handlebars (critical severity) as of 2026-04-07"
13
+ - vulnerability: GHSA-xjpj-3mr7-gcpf
14
+ include-aliases: true
15
+ reason: "Unfixable npm transitive vulnerability: handlebars (high severity) as of 2026-04-07"
16
+ - vulnerability: GHSA-3mfm-83xf-c92r
17
+ include-aliases: true
18
+ reason: "Unfixable npm transitive vulnerability: handlebars (high severity) as of 2026-04-07"
19
+ - vulnerability: GHSA-xhpv-hc6g-r9c6
20
+ include-aliases: true
21
+ reason: "Unfixable npm transitive vulnerability: handlebars (high severity) as of 2026-04-07"
22
+ - vulnerability: GHSA-25h7-pfq9-p65f
23
+ include-aliases: true
24
+ reason: "Unfixable npm transitive vulnerability: flatted (high severity) as of 2026-04-07"
25
+ - vulnerability: GHSA-9cx6-37pm-9jff
26
+ include-aliases: true
27
+ reason: "Unfixable npm transitive vulnerability: handlebars (high severity) as of 2026-04-07"
28
+ - vulnerability: GHSA-rf6f-7fwh-wjgh
29
+ include-aliases: true
30
+ reason: "Unfixable npm transitive vulnerability: flatted (high severity) as of 2026-04-07"
31
+ - vulnerability: GHSA-7rx3-28cr-v5wh
32
+ include-aliases: true
33
+ reason: "Unfixable npm transitive vulnerability: handlebars (moderate severity) as of 2026-04-07"
34
+ - vulnerability: GHSA-2qvq-rjwj-gvw9
35
+ include-aliases: true
36
+ reason: "Unfixable npm transitive vulnerability: handlebars (moderate severity) as of 2026-04-07"
37
+ - vulnerability: GHSA-442j-39wm-28r2
38
+ include-aliases: true
39
+ reason: "Unfixable npm transitive vulnerability: handlebars (low severity) as of 2026-04-07"
40
+ - vulnerability: GHSA-44fc-8fm5-q62h
41
+ include-aliases: true
42
+ reason: "Unfixable npm transitive vulnerability: unknown (unknown severity) as of 2026-04-07"
43
+ - vulnerability: GHSA-hf2r-9gf9-rwch
44
+ include-aliases: true
45
+ reason: "Unfixable npm transitive vulnerability: unknown (unknown severity) as of 2026-04-07"
46
+ - vulnerability: GHSA-48c2-rrv3-qjmp
47
+ include-aliases: true
48
+ reason: "Unfixable npm transitive vulnerability: unknown (unknown severity) as of 2026-04-07"
49
+ - vulnerability: GHSA-r4q5-vmmm-2653
50
+ include-aliases: true
51
+ reason: "Unfixable npm transitive vulnerability: follow-redirects (moderate severity) as of 2026-04-21"
52
+ - vulnerability: GHSA-xq3m-2v4x-88gg
53
+ include-aliases: true
54
+ reason: "Unfixable npm transitive vulnerability: @mojaloop/event-sdk>protobufjs (moderate severity) as of 2026-04-21"
21
55
  output:
22
- - "table" # Human-readable table format
23
- - "json" # Machine-readable JSON for further processing
24
-
25
- # Grype configuration options
26
- quiet: false # Show progress and status messages
27
- check-for-app-update: false # Don't check for Grype updates during CI
28
- only-fixed: false # Show all vulnerabilities, not just those with fixes
29
- add-cpes-if-none: false # Don't add CPEs if none are found
30
- by-cve: false # Group by vulnerability rather than CVE
31
-
32
- # Database settings
56
+ - table
57
+ - json
58
+ quiet: false
59
+ check-for-app-update: false
60
+ only-fixed: false
61
+ add-cpes-if-none: false
62
+ by-cve: false
33
63
  db:
34
- auto-update: true # Auto-update the vulnerability database
35
- validate-age: true # Validate the age of the vulnerability database
36
- max-allowed-built-age: 120h # Maximum age of the vulnerability database (5 days)
37
-
38
- # Severity thresholds (handled by the orb, but documented here for clarity)
39
- # The build will fail on Critical, High, or Medium severity vulnerabilities
40
- # Low and Negligible severities are reported but won't fail the build
64
+ auto-update: true
65
+ validate-age: true
66
+ max-allowed-built-age: 120h
package/.ncurc.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  reject: [
3
3
  # TODO: Added "@hapi/catbox-memory" to ncurc for dep:check to ignore updates due to breaking changes which should be handled by another story
4
4
  "@hapi/catbox-memory",
5
- "ioredis" # version 5.7.0 caused failures of unit-tests in QS
5
+ "ioredis", # version 5.7.0 caused failures of unit-tests in QS
6
6
  ]
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 22.15.1
1
+ 22.22.2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [18.36.0](https://github.com/mojaloop/central-services-shared/compare/v18.35.7...v18.36.0) (2026-05-13)
6
+
7
+
8
+ ### Features
9
+
10
+ * add new endpoints for tppConsentRequest ([#519](https://github.com/mojaloop/central-services-shared/issues/519)) ([635a383](https://github.com/mojaloop/central-services-shared/commit/635a3830b22ee1e01914b9a17be6df802772dc3c))
11
+ * add tppConsents request endpoints ([#520](https://github.com/mojaloop/central-services-shared/issues/520)) ([5fb9873](https://github.com/mojaloop/central-services-shared/commit/5fb98739f330736ca74d3a9934dbf971d4c46c0b))
12
+
13
+ ### [18.35.7](https://github.com/mojaloop/central-services-shared/compare/v18.35.6...v18.35.7) (2026-04-07)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * require Accept header for initiating methods per FSPIOP spec ([#516](https://github.com/mojaloop/central-services-shared/issues/516)) ([4d29c23](https://github.com/mojaloop/central-services-shared/commit/4d29c23464ceaf31961991fa82d6ac986d47e4d4)), closes [mojaloop/project#4183](https://github.com/mojaloop/project/issues/4183)
19
+
20
+ ### [18.35.6](https://github.com/mojaloop/central-services-shared/compare/v18.35.5...v18.35.6) (2026-02-26)
21
+
22
+
23
+ ### Chore
24
+
25
+ * rm circular dependency on sdk-standard-components ([#510](https://github.com/mojaloop/central-services-shared/issues/510)) ([7346920](https://github.com/mojaloop/central-services-shared/commit/7346920e3c3e0996aeebfd7cce4e24ac54d59313))
26
+
27
+ ### [18.35.5](https://github.com/mojaloop/central-services-shared/compare/v18.35.4...v18.35.5) (2026-02-20)
28
+
29
+
30
+ ### Chore
31
+
32
+ * update orb and dep ver ([#509](https://github.com/mojaloop/central-services-shared/issues/509)) ([7293cff](https://github.com/mojaloop/central-services-shared/commit/7293cffea15145d115b25625eb4352ceb651bb35))
33
+
34
+ ### [18.35.4](https://github.com/mojaloop/central-services-shared/compare/v18.35.3...v18.35.4) (2026-02-19)
35
+
36
+
37
+ ### Chore
38
+
39
+ * update dependencies, node.js 22.22.0, and security patches ([#508](https://github.com/mojaloop/central-services-shared/issues/508)) ([51281d8](https://github.com/mojaloop/central-services-shared/commit/51281d8eaaa8b9da53214f2b6543d1aef165a682))
40
+
5
41
  ### [18.35.3](https://github.com/mojaloop/central-services-shared/compare/v18.35.2...v18.35.3) (2026-02-06)
6
42
 
7
43
 
package/audit-ci.jsonc CHANGED
@@ -4,6 +4,5 @@
4
4
  // Only use one of ["low": true, "moderate": true, "high": true, "critical": true]
5
5
  "moderate": true,
6
6
  "allowlist": [ // NOTE: Please add as much information as possible to any items added to the allowList
7
- // e.g. Currently no fixes available for the following
8
7
  ]
9
- }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-services-shared",
3
- "version": "18.36.0-snapshot.3",
3
+ "version": "18.36.0",
4
4
  "description": "Shared code for mojaloop central services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -48,7 +48,6 @@
48
48
  "test:endpoints": "npx tape 'test/unit/util/endpoints.test.js'",
49
49
  "test:mysql": "npx tape 'test/unit/mysql/**/*.test.js'",
50
50
  "test:participants": "npx tape 'test/unit/util/participants.test.js'",
51
- "test:request": "npx tape 'test/unit/util/request.test.js'",
52
51
  "test:trans": "npx tape 'test/unit/util/headers/transformer.test.js'",
53
52
  "test:unit": "npx tape 'test/unit/**/*.test.js' | tap-spec",
54
53
  "test:xunit": "npx tape 'test/unit/**/**.test.js' | tap-xunit > ./test/results/xunit.xml",
@@ -68,26 +67,25 @@
68
67
  "dependencies": {
69
68
  "@hapi/catbox": "12.1.1",
70
69
  "@hapi/catbox-memory": "5.0.1",
71
- "@hapi/hapi": "21.4.4",
70
+ "@hapi/hapi": "21.4.9",
72
71
  "@hapi/joi-date": "2.0.1",
73
- "@mojaloop/inter-scheme-proxy-cache-lib": "2.9.0",
74
- "@opentelemetry/api": "1.9.0",
75
- "@opentelemetry/semantic-conventions": "1.39.0",
72
+ "@mojaloop/inter-scheme-proxy-cache-lib": "2.10.0",
73
+ "@opentelemetry/api": "1.9.1",
76
74
  "async-exit-hook": "2.0.1",
77
75
  "async-retry": "1.3.3",
78
- "axios": "1.13.4",
76
+ "axios": "1.16.1",
79
77
  "clone": "2.1.2",
80
- "convict": "^6.2.4",
81
- "dotenv": "17.2.4",
78
+ "convict": "6.2.5",
79
+ "dotenv": "17.4.2",
82
80
  "env-var": "7.5.0",
83
81
  "event-stream": "4.0.1",
84
82
  "fast-safe-stringify": "2.1.1",
85
- "immutable": "5.1.4",
83
+ "immutable": "5.1.5",
86
84
  "ioredis": "5.6.1",
87
- "joi": "18.0.2",
88
- "lodash": "4.17.23",
85
+ "joi": "18.2.1",
86
+ "lodash": "4.18.1",
89
87
  "mustache": "4.2.0",
90
- "openapi-backend": "5.15.0",
88
+ "openapi-backend": "5.16.1",
91
89
  "raw-body": "3.0.2",
92
90
  "rc": "1.2.8",
93
91
  "redlock": "5.0.0-beta.2",
@@ -95,30 +93,29 @@
95
93
  "ulidx": "2.4.1",
96
94
  "uuid4": "2.0.3",
97
95
  "widdershins": "4.0.1",
98
- "yaml": "2.8.2"
96
+ "yaml": "2.9.0"
99
97
  },
100
98
  "devDependencies": {
101
- "@mojaloop/central-services-error-handling": "13.1.5",
102
- "@mojaloop/central-services-logger": "11.11.0-snapshot.0",
103
- "@mojaloop/central-services-metrics": "12.8.3",
104
- "@mojaloop/event-sdk": "14.8.2",
105
- "@mojaloop/sdk-standard-components": "19.18.7",
106
- "@opentelemetry/auto-instrumentations-node": "^0.69.0",
99
+ "@mojaloop/central-services-error-handling": "13.1.6",
100
+ "@mojaloop/central-services-logger": "11.10.4",
101
+ "@mojaloop/central-services-metrics": "12.8.5",
102
+ "@mojaloop/event-sdk": "14.8.4",
103
+ "@opentelemetry/auto-instrumentations-node": "^0.76.0",
107
104
  "@types/hapi__joi": "17.1.15",
108
- "ajv": "^8.17.1",
105
+ "ajv": "8.20.0",
109
106
  "ajv-formats": "^3.0.1",
110
107
  "ajv-keywords": "^5.1.0",
111
108
  "audit-ci": "7.1.0",
112
109
  "base64url": "3.0.1",
113
110
  "chance": "1.1.13",
114
- "npm-check-updates": "19.3.2",
115
- "nyc": "17.1.0",
111
+ "npm-check-updates": "22.2.0",
112
+ "nyc": "18.0.0",
116
113
  "portfinder": "1.0.38",
117
- "pre-commit": "1.2.2",
114
+ "pre-commit": "2.0.0",
118
115
  "proxyquire": "2.1.3",
119
116
  "replace": "1.2.2",
120
117
  "rewire": "9.0.1",
121
- "sinon": "21.0.1",
118
+ "sinon": "22.0.0",
122
119
  "standard": "17.1.2",
123
120
  "standard-version": "9.5.0",
124
121
  "tap-spec": "5.0.0",
@@ -127,15 +124,13 @@
127
124
  "tapes": "4.1.0"
128
125
  },
129
126
  "overrides": {
130
- "@mojaloop/central-services-logger": "$@mojaloop/central-services-logger",
131
- "axios": "1.13.4",
132
- "qs": "6.14.1",
133
- "brace-expansion": "2.0.2",
134
- "form-data": "4.0.4",
135
- "nanoid": "5.1.5",
136
- "postcss": {
137
- "nanoid": "5.1.5"
138
- },
127
+ "axios": "1.16.1",
128
+ "qs": "6.14.2",
129
+ "brace-expansion": "1.1.13",
130
+ "form-data": "4.0.5",
131
+ "convict": "6.2.5",
132
+ "nanoid": "3.3.11",
133
+ "postcss": "8.5.10",
139
134
  "shins": {
140
135
  "ejs": "3.1.10",
141
136
  "sanitize-html": "2.12.1",
@@ -148,15 +143,22 @@
148
143
  "swagger2openapi": "7.0.8"
149
144
  },
150
145
  "markdown-it": "12.3.2",
151
- "fast-xml-parser": "5.3.4",
146
+ "fast-xml-parser": "5.7.0",
152
147
  "trim": "0.0.3",
153
148
  "cross-spawn": "7.0.6",
154
149
  "yargs-parser": "21.1.1",
155
150
  "jws": "3.2.3",
156
151
  "validator": "13.15.22",
157
- "lodash": "4.17.23",
158
- "lodash-es": "4.17.23",
159
- "undici": "7.18.2"
152
+ "lodash": "4.18.1",
153
+ "lodash-es": "4.18.1",
154
+ "undici": "6.25.0",
155
+ "@hapi/content": "6.0.1",
156
+ "replace": {
157
+ "minimatch": "3.1.4"
158
+ },
159
+ "path-to-regexp": "0.1.13",
160
+ "picomatch": "2.3.2",
161
+ "yaml": "2.9.0"
160
162
  },
161
163
  "peerDependencies": {
162
164
  "@mojaloop/central-services-error-handling": "13.x.x",
package/src/config.js CHANGED
@@ -9,13 +9,6 @@ const config = convict({
9
9
  env: 'SHARED_CACHE_LOG_LEVEL'
10
10
  },
11
11
 
12
- httpLogLevel: {
13
- doc: 'Log level for HTTP wrapper.',
14
- format: logLevelValues,
15
- default: logLevelsMap.warn,
16
- env: 'LOG_LEVEL_HTTP'
17
- },
18
-
19
12
  defaultTtlSec: {
20
13
  doc: 'Default cache TTL.',
21
14
  format: Number,
@@ -107,7 +107,17 @@ const FspEndpointTypes = {
107
107
  TPP_CB_URL_ACCOUNT_REQUEST_PUT_ERROR: 'TPP_CB_URL_ACCOUNT_REQUEST_PUT_ERROR',
108
108
  TPP_CB_URL_ACCOUNT_REQUEST_GET: 'TPP_CB_URL_ACCOUNT_REQUEST_GET',
109
109
  TPP_CB_URL_ACCOUNTS_GET: 'TPP_CB_URL_ACCOUNTS_GET',
110
- TPP_CB_URL_ACCOUNTS_PUT: 'TPP_CB_URL_ACCOUNTS_PUT'
110
+ TPP_CB_URL_ACCOUNTS_PUT: 'TPP_CB_URL_ACCOUNTS_PUT',
111
+ TPP_CB_URL_CONSENT_REQUEST_POST: 'TPP_CB_URL_CONSENT_REQUEST_POST',
112
+ TPP_CB_URL_CONSENT_REQUEST_GET: 'TPP_CB_URL_CONSENT_REQUEST_GET',
113
+ TPP_CB_URL_CONSENT_REQUEST_PUT: 'TPP_CB_URL_CONSENT_REQUEST_PUT',
114
+ TPP_CB_URL_CONSENT_REQUEST_PATCH: 'TPP_CB_URL_CONSENT_REQUEST_PATCH',
115
+ TPP_CB_URL_CONSENT_REQUEST_PUT_ERROR: 'TPP_CB_URL_CONSENT_REQUEST_PUT_ERROR',
116
+ TPP_CB_URL_CONSENTS_POST: 'TPP_CB_URL_CONSENTS_POST',
117
+ TPP_CB_URL_CONSENTS_GET: 'TPP_CB_URL_CONSENTS_GET',
118
+ TPP_CB_URL_CONSENTS_PUT: 'TPP_CB_URL_CONSENTS_PUT',
119
+ TPP_CB_URL_CONSENTS_PUT_ERROR: 'TPP_CB_URL_CONSENTS_PUT_ERROR',
120
+ TPP_CB_URL_CONSENTS_DELETE: 'TPP_CB_URL_CONSENTS_DELETE'
111
121
  }
112
122
 
113
123
  const FspEndpointTemplates = {
@@ -176,7 +186,17 @@ const FspEndpointTemplates = {
176
186
  TPP_ACCOUNT_REQUEST_GET: '/tppAccountRequest/{{ID}}',
177
187
  TPP_ACCOUNTS_GET: '/tppAccounts/{{ID}}/{{SignedChallenge}}',
178
188
  TPP_ACCOUNTS_PUT: '/tppAccounts/{{ID}}',
179
- TPP_ACCOUNTS_PUT_ERROR: '/tppAccounts/{{ID}}/error'
189
+ TPP_ACCOUNTS_PUT_ERROR: '/tppAccounts/{{ID}}/error',
190
+ TPP_CONSENT_REQUEST_POST: '/tppConsentRequests',
191
+ TPP_CONSENT_REQUEST_GET: '/tppConsentRequests/{{ID}}',
192
+ TPP_CONSENT_REQUEST_PUT: '/tppConsentRequests/{{ID}}',
193
+ TPP_CONSENT_REQUEST_PATCH: '/tppConsentRequests/{{ID}}',
194
+ TPP_CONSENT_REQUEST_PUT_ERROR: '/tppConsentRequests/{{ID}}/error',
195
+ TPP_CONSENTS_POST: '/tppConsents',
196
+ TPP_CONSENTS_GET: '/tppConsents/{{ID}}',
197
+ TPP_CONSENTS_PUT: '/tppConsents/{{ID}}',
198
+ TPP_CONSENTS_PUT_ERROR: '/tppConsents/{{ID}}/error',
199
+ TPP_CONSENTS_DELETE: '/tppConsents/{{ID}}'
180
200
  }
181
201
 
182
202
  module.exports = {
package/src/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Utils as HapiUtil, Server } from '@hapi/hapi'
2
2
  import { ILogger } from '@mojaloop/central-services-logger/src/contextLogger'
3
3
  import { Knex } from 'knex';
4
- import { AxiosRequestConfig, AxiosResponse, ResponseType as AxiosResponseType } from 'axios'
5
4
  import IORedis from 'ioredis';
6
5
 
7
6
  declare namespace CentralServicesShared {
@@ -764,27 +763,9 @@ declare namespace CentralServicesShared {
764
763
  accept: string
765
764
  }
766
765
 
767
- type RequestParams = {
768
- url: string,
769
- headers: HapiUtil.Dictionary<string>,
770
- source: string,
771
- destination?: string,
772
- hubNameRegex: RegExp,
773
- method?: RestMethodsEnum,
774
- payload?: any,
775
- params?: AxiosRequestConfig['params'],
776
- responseType?: AxiosResponseType,
777
- span?: any,
778
- jwsSigner?: any,
779
- protocolVersions?: ProtocolVersionsType,
780
- apiType?: ApiTypeValues,
781
- axiosRequestOptionsOverride?: Partial<AxiosRequestConfig>,
782
- logger?: ILogger,
783
- peerService?: string
784
- }
785
- export interface Request {
786
- sendRequest(params: RequestParams): Promise<AxiosResponse>
787
- sendBaseRequest(params?: AxiosRequestConfig & { logger?: ILogger, peerService?: string }): Promise<AxiosResponse>
766
+ type RequestParams = { url: string, headers: HapiUtil.Dictionary<string>, source: string, destination: string, hubNameRegex: RegExp, method?: RestMethodsEnum, payload?: any, responseType?: string, span?: any, jwsSigner?: any, protocolVersions?: ProtocolVersionsType }
767
+ interface Request {
768
+ sendRequest(params: RequestParams): Promise<any>
788
769
  }
789
770
 
790
771
  interface Kafka {
@@ -81,9 +81,10 @@ const plugin = {
81
81
 
82
82
  if (needProxySourceValidation) validateProxySourceHeaders(request.headers)
83
83
 
84
- // Always validate the accept header for a get request, or optionally if it has been
85
- // supplied
86
- if (request.method.toLowerCase() === 'get' || request.headers.accept) {
84
+ // Require accept header for request-initiating methods (GET, POST, DELETE)
85
+ // per FSPIOP API spec. PUT/PATCH callbacks do not require Accept.
86
+ const methodRequiresAccept = ['get', 'post', 'delete'].includes(request.method.toLowerCase())
87
+ if (methodRequiresAccept || request.headers.accept) {
87
88
  if (request.headers.accept === undefined) {
88
89
  throw createFSPIOPError(Enums.FSPIOPErrorCodes.MISSING_ELEMENT, errorMessages.REQUIRE_ACCEPT_HEADER)
89
90
  }
@@ -30,7 +30,6 @@
30
30
  const { env } = require('node:process')
31
31
  const { asyncStorage } = require('@mojaloop/central-services-logger/src/contextLogger')
32
32
  const { logger } = require('../../../logger')
33
- const { incomingRequestAttributesDto } = require('../../otelDto')
34
33
 
35
34
  const INTERNAL_ROUTES = env.LOG_INTERNAL_ROUTES ? env.LOG_INTERNAL_ROUTES.split(',') : ['/health', '/metrics', '/live']
36
35
  const TRACE_ID_HEADER = env.LOG_TRACE_ID_HEADER ?? 'traceid'
@@ -62,11 +61,13 @@ const loggingPlugin = {
62
61
  server.ext({
63
62
  type: 'onRequest',
64
63
  method: (request, h) => {
65
- const requestId = request.info.id = `${request.info.id}__${request.headers[traceIdHeader]}`
64
+ const { path, method, headers, payload, query } = request
65
+ const { remoteAddress } = request.info
66
+ const requestId = request.info.id = `${request.info.id}__${headers[traceIdHeader]}`
66
67
  asyncStorage.enterWith({ requestId })
67
68
 
68
- if (shouldLog(request.path)) {
69
- extractRequestOtelContext(request, log)
69
+ if (shouldLog(path)) {
70
+ log.info(`[==> req] ${method.toUpperCase()} ${path}`, { headers, payload, query, remoteAddress })
70
71
  }
71
72
  return h.continue
72
73
  }
@@ -76,7 +77,16 @@ const loggingPlugin = {
76
77
  type: 'onPreResponse',
77
78
  method: (request, h) => {
78
79
  if (shouldLog(request.path)) {
79
- extractResponseOtelContext(request, log)
80
+ const { path, method, payload, response } = request
81
+ const { received } = request.info
82
+
83
+ const statusCode = response instanceof Error
84
+ ? response.output?.statusCode
85
+ : response.statusCode
86
+ const { output } = response
87
+ const respTimeSec = ((Date.now() - received) / 1000).toFixed(1)
88
+
89
+ log.info(`[<== ${statusCode}] ${method.toUpperCase()} ${path} [${respTimeSec} sec]`, { payload, output })
80
90
  }
81
91
  return h.continue
82
92
  }
@@ -84,71 +94,4 @@ const loggingPlugin = {
84
94
  }
85
95
  }
86
96
 
87
- /**
88
- * @param {import('@hapi/hapi').Request} request
89
- * @param {ILogger} log
90
- * @returns OTelAttributes
91
- */
92
- const extractRequestOtelContext = (request, log) => {
93
- const { method, path, headers, payload } = request
94
-
95
- log.info(`[==> req] ${method.toUpperCase()} ${path} `, {
96
- headers: extractHeadersForLogs(headers),
97
- payload, // check if payload has already been parsed by this moment
98
- ...extractAttributes({ request })
99
- })
100
- }
101
-
102
- /**
103
- * @param {import('@hapi/hapi').Request} request
104
- * @param {ILogger} log
105
- * @returns OTelAttributes
106
- */
107
- const extractResponseOtelContext = (request, log) => {
108
- const { method, path, response } = request
109
-
110
- const statusCode = response instanceof Error
111
- ? response.output?.statusCode
112
- : response?.statusCode
113
-
114
- const errorType = response instanceof Error
115
- ? response.output?.payload?.error
116
- : undefined
117
-
118
- const durationSec = (Date.now() - request.info.received) / 1000
119
-
120
- log.info(`[<== ${statusCode}] ${method.toUpperCase()} ${path} [${durationSec} sec]`, {
121
- headers: extractHeadersForLogs(response?.output?.headers),
122
- payload: response?.output?.payload,
123
- ...extractAttributes({ request, durationSec, statusCode, errorType })
124
- })
125
- }
126
-
127
- const extractAttributes = ({ request, durationSec, statusCode, errorType }) => {
128
- return incomingRequestAttributesDto({
129
- method: request.method,
130
- path: request.path,
131
- url: getFullUrl(request),
132
- route: request.route.path,
133
- serverAddress: request.info.hostname,
134
- clientAddress: request.info.remoteAddress,
135
- userAgent: request.headers['user-agent'],
136
- requestId: request.info.id,
137
- durationSec,
138
- statusCode,
139
- errorType
140
- })
141
- }
142
-
143
- /** @param {import('@hapi/hapi').Request} req */
144
- const getFullUrl = (req) => {
145
- const search = req.url?.search || ''
146
- return `${req.server.info.protocol}://${req.info.host}${req.path}${search}`
147
- }
148
-
149
- const extractHeadersForLogs = (headers = {}) => {
150
- // todo: add impl.
151
- return headers
152
- }
153
-
154
97
  module.exports = loggingPlugin
@@ -30,31 +30,29 @@
30
30
  'use strict'
31
31
 
32
32
  const http = require('node:http')
33
- const axios = require('axios')
33
+ const request = require('axios')
34
34
  const stringify = require('fast-safe-stringify')
35
35
  const EventSdk = require('@mojaloop/event-sdk')
36
36
  const ErrorHandler = require('@mojaloop/central-services-error-handling')
37
37
  const Metrics = require('@mojaloop/central-services-metrics')
38
-
39
- const { logger: globalLogger } = require('../logger')
38
+ const Headers = require('./headers/transformer')
39
+ const enums = require('../enums')
40
+ const { logger } = require('../logger')
40
41
  const { API_TYPES } = require('../constants')
41
42
  const config = require('../config')
42
- const enums = require('../enums')
43
- const Headers = require('./headers/transformer')
44
- const { outgoingRequestAttributesDto } = require('./otelDto')
45
43
 
46
44
  const MISSING_FUNCTION_PARAMETERS = 'Missing parameters for function'
47
45
 
48
46
  // Delete the default headers that the `axios` module inserts as they can brake our conventions.
49
47
  // By default it would insert `"Accept":"application/json, text/plain, */*"`.
50
- delete axios.defaults.headers.common.Accept
48
+ delete request.defaults.headers.common.Accept
51
49
 
52
50
  const keepAlive = (process.env.HTTP_AGENT_KEEP_ALIVE ?? 'true') === 'true'
53
- globalLogger.verbose('http keepAlive:', { keepAlive })
51
+ logger.verbose('http keepAlive:', { keepAlive })
54
52
 
55
53
  // Enable keepalive for http
56
- axios.defaults.httpAgent = new http.Agent({ keepAlive })
57
- axios.defaults.httpAgent.toJSON = () => ({})
54
+ request.defaults.httpAgent = new http.Agent({ keepAlive })
55
+ request.defaults.httpAgent.toJSON = () => ({})
58
56
 
59
57
  /**
60
58
  * @function sendRequest
@@ -79,12 +77,11 @@ axios.defaults.httpAgent.toJSON = () => ({})
79
77
  * @param {SendRequestProtocolVersions | undefined} protocolVersions the config for Protocol versions to be used
80
78
  * @param {'fspiop' | 'iso20022'} apiType the API type of the request being sent
81
79
  * @param {object} axiosRequestOptionsOverride axios request options to override https://axios-http.com/docs/req_config
82
- * @param {ILogger} [logger] ContextLogger instance with specific context
83
- * @param {string} [peerService] Logical service name to call (for OTel)
84
80
  * @param {regex} hubNameRegex hubName Regex
85
81
  *
86
82
  *@return {Promise<any>} The response for the request being sent or error object with response included
87
83
  */
84
+
88
85
  const sendRequest = async ({
89
86
  url,
90
87
  headers,
@@ -99,8 +96,6 @@ const sendRequest = async ({
99
96
  protocolVersions = undefined,
100
97
  apiType = API_TYPES.fspiop,
101
98
  axiosRequestOptionsOverride = {},
102
- logger = createHttpLogger(),
103
- peerService = '',
104
99
  hubNameRegex
105
100
  }) => {
106
101
  const histTimerEnd = Metrics.getHistogram(
@@ -118,9 +113,6 @@ const sendRequest = async ({
118
113
  // think, if we can just avoid checking "destination"
119
114
  throw ErrorHandler.Factory.createInternalServerFSPIOPError(MISSING_FUNCTION_PARAMETERS)
120
115
  }
121
-
122
- const log = logger.child({ component: 'httpRequest' })
123
-
124
116
  try {
125
117
  const transformedHeaders = Headers.transformHeaders(headers, {
126
118
  httpMethod: method,
@@ -134,10 +126,9 @@ const sendRequest = async ({
134
126
  url,
135
127
  method,
136
128
  headers: transformedHeaders,
137
- data: payload,
129
+ data: payload, // todo: think, if it's better to transform to ISO format here (based on apiType)
138
130
  params,
139
131
  responseType,
140
- peerService,
141
132
  timeout: config.get('httpRequestTimeoutMs'),
142
133
  ...axiosRequestOptionsOverride
143
134
  }
@@ -158,18 +149,14 @@ const sendRequest = async ({
158
149
  }
159
150
  span.audit({ ...rest, payload }, EventSdk.AuditEventAction.egress)
160
151
  }
161
-
162
- const response = await sendBaseRequest({
163
- ...requestOptions,
164
- logger: log,
165
- peerService
166
- })
152
+ logger.debug('sendRequest::requestOptions:', { requestOptions })
153
+ const response = await request(requestOptions)
167
154
 
168
155
  !!sendRequestSpan && await sendRequestSpan.finish()
169
156
  histTimerEnd({ success: true, source, destination, method })
170
157
  return response
171
158
  } catch (error) {
172
- log.error('error in request.sendRequest:', {
159
+ logger.error('error in request.sendRequest:', {
173
160
  code: error.code,
174
161
  message: error.message,
175
162
  stack: error.stack,
@@ -260,61 +247,6 @@ const sendRequest = async ({
260
247
  }
261
248
  }
262
249
 
263
- // todo: think better name
264
- // it's for http calls without params validation and transformHeaders
265
- const sendBaseRequest = async ({
266
- logger = createHttpLogger(),
267
- peerService = '',
268
- ...reqOptions
269
- } = {}) => {
270
- const log = logger.child({ component: 'sendBaseRequest' })
271
- const { method, url } = reqOptions
272
- const methodUrl = `${method?.toUpperCase()} ${url}`
273
- const startTime = Date.now()
274
-
275
- let statusCode
276
- let errorType
277
-
278
- try {
279
- log.debug(`[-->] options for ${methodUrl}: `, { reqOptions })
280
-
281
- const response = await axios(reqOptions)
282
-
283
- statusCode = response?.status
284
- log.verbose(`[<--] details of ${methodUrl}: `, {
285
- data: response?.data,
286
- headers: response?.headers, // todo: extract only needed headers
287
- statusCode,
288
- reqOptions
289
- })
290
-
291
- return response
292
- } catch (error) {
293
- statusCode = error.response?.status
294
- errorType = error.code
295
- throw error // todo: think, if we need to rethrow our custom error here
296
- } finally {
297
- const severity = typeof statusCode === 'number'
298
- ? (statusCode >= 200 && statusCode < 300 ? 'info' : 'warn')
299
- : 'error'
300
- log[severity](`[<--] ${methodUrl} [${statusCode || errorType || 'N/A'}]: `, outgoingRequestAttributesDto({
301
- method,
302
- url,
303
- statusCode,
304
- durationSec: (Date.now() - startTime) / 1000,
305
- errorType,
306
- peerService
307
- }))
308
- }
309
- }
310
-
311
- const createHttpLogger = () => {
312
- const logger = globalLogger.child()
313
- logger.setLevel(config.get('httpLogLevel'))
314
- return logger
315
- }
316
-
317
250
  module.exports = {
318
- sendRequest,
319
- sendBaseRequest
251
+ sendRequest
320
252
  }
@@ -151,7 +151,8 @@ Test('headerValidation plugin test', async (pluginTest) => {
151
151
  t.end()
152
152
  })
153
153
 
154
- pluginTest.test('accept validation is not performed on post, put requests without an accept header', async t => {
154
+ pluginTest.test('accept validation is performed on post requests without an accept header', async t => {
155
+ const fspiopCode = ErrorHandling.Enums.FSPIOPErrorCodes.MISSING_ELEMENT
155
156
  const opts = {
156
157
  url: `/${resource}`,
157
158
  headers: {
@@ -159,11 +160,25 @@ Test('headerValidation plugin test', async (pluginTest) => {
159
160
  date: new Date().toUTCString()
160
161
  }
161
162
  }
162
- await Promise.all(['post', 'put'].map(async method => {
163
- const res = await server.inject({ ...opts, method })
164
- t.is(res.payload, '')
165
- t.is(res.statusCode, 202)
166
- }))
163
+ const res = await server.inject({ ...opts, method: 'post' })
164
+ t.is(res.statusCode, fspiopCode.httpStatusCode)
165
+ const payload = JSON.parse(res.payload)
166
+ t.is(payload.apiErrorCode.code, fspiopCode.code)
167
+ t.is(payload.message, errorMessages.REQUIRE_ACCEPT_HEADER)
168
+ t.end()
169
+ })
170
+
171
+ pluginTest.test('accept validation is not required for put requests without an accept header', async t => {
172
+ const opts = {
173
+ url: `/${resource}`,
174
+ headers: {
175
+ 'content-type': generateContentTypeHeader(resource, 1),
176
+ date: new Date().toUTCString()
177
+ }
178
+ }
179
+ const res = await server.inject({ ...opts, method: 'put' })
180
+ t.is(res.payload, '')
181
+ t.is(res.statusCode, 202)
167
182
  t.end()
168
183
  })
169
184
 
@@ -134,7 +134,7 @@ Tape('loggingPlugin Tests -->', (pluginTests) => {
134
134
  t.true(statusCode === 500, 'handler failed')
135
135
  t.true(log.info.callCount === 2, 'log request/response')
136
136
  t.true(log.info.lastCall.firstArg.startsWith('[<== 500]'), 'error code is logged')
137
- t.ok(log.info.lastCall.lastArg.payload, 'error output is logged')
137
+ t.ok(log.info.lastCall.lastArg.output.payload, 'error output is logged')
138
138
  }))
139
139
 
140
140
  pluginTests.test('should not log requests on internal routes', tryCatchEndTest(async t => {
@@ -10,35 +10,6 @@ const Enum = require('../../../src/enums')
10
10
  const Helper = require('../../util/helper')
11
11
  const Metrics = require('@mojaloop/central-services-metrics')
12
12
  const Uuid = require('uuid4')
13
- const JwsSigner = require('@mojaloop/sdk-standard-components').Jws.signer
14
-
15
- const signingKey = `-----BEGIN RSA PRIVATE KEY-----
16
- MIIEowIBAAKCAQEA0eJEh3Op5p6x137lRkAsvmEBbd32dbRChrCUItZbtxjf/qfB
17
- yD5k8Hn4n4vbqzP8XSGS0f6KmNC+iRaP74HVgzAqc4Uid4J8dtSBq3VmucYQYzLc
18
- 101QjuvD+SKmZwlw/q0PtulmqlASI2SbMfwcAraMi6ab7v5W4EGNeIPLEIo3BXsQ
19
- DTCWqiZb7aXkHkcY7sOjAzK/2bNGYFmAthdYrHzvCkqnJ7LAHX3Oj7rJea5MqtuN
20
- B9POZYaD10n9JuYWdwPqLrw6/hVgPSFEy+ulrVbXf54ZH0dfMThAYRvFrT81yulk
21
- H95JhXWGdi6cTp6t8LVOKFhnNfxjWw0Jayj9xwIDAQABAoIBADB2u/Y/CgNbr5sg
22
- DRccqHhJdAgHkep59kadrYch0knEL6zg1clERxCUSYmlxNKSjXp/zyQ4T46b3PNQ
23
- x2m5pDDHxXWpT10jP1Q9G7gYwuCw0IXnb8EzdB+cZ0M28g+myXW1RoSo/nDjTlzn
24
- 1UJEgb9Kocd5cFZOWocr+9vRKumlZULMsA8yiNwlAfJHcMBM7acsa3myCqVhLyWt
25
- 4BQylVuLFa+A6QzpMXEwFCq8EOXf07gl1XVzC6LJ1fTa9gVM3N+YE+oEXKrsHCxG
26
- /ACgKsjepL27QjJ7qvecWPP0F2LxEZYOm5tbXaKJTobzQUJHgUokanZMhjYprDsZ
27
- zumLw9kCgYEA/DUWcnLeImlfq/EYdhejkl3J+WX3vhS23OqVgY1amu7CZzaai6vt
28
- H0TRc8Zsbi4jgmFDU8PFzytP6qz6Tgom4R736z6oBi7bjnGyN17/NSbf+DaRVcM6
29
- vnZr7jNC2FJlECmIN+dkwUA/YCr2SA7hxZXM9mIYSc+6+glDiIO5Cf0CgYEA1Qo/
30
- uQbVHhW+Cp8H0kdMuhwUbkBquRrxRZlXS1Vrf3f9me9JLUy9UPWb3y3sKVurG5+O
31
- SIlr4hDcZyXdE198MtDMhBIGqU9ORSjppJDNDVvtt+n2FD4XmWIU70vKBJBivX0+
32
- Bow6yduis+p12fuvpvpnKCz8UjOgOQJhLZ4GQBMCgYBP6gpozVjxkm4ML2LO2IKt
33
- +CXtbo/nnOysZ3BkEoQpH4pd5gFmTF3gUJAFnVPyPZBm2abZvejJ0jGKbLELVVAo
34
- eQWZdssK2oIbSo9r2CAJmX3SSogWorvUafWdDoUZwlHfoylUfW+BhHgQYsyS3JRR
35
- ZTwCveZwTPA0FgdeFE7niQKBgQCHaD8+ZFhbCejDqXb4MXdUJ3rY5Lqwsq491YwF
36
- huKPn32iNNQnJcqCxclv3iln1Cr6oLx34Fig1KSyLv/IS32OcuY635Y6UPznumxe
37
- u+aJIjADIILXNOwdAplZy6s4oWkRFaSx1rmbCa3tew2zImTv1eJxR76MpOGmupt3
38
- uiQw3wKBgFjBT/aVKdBeHeP1rIHHldQV5QQxZNkc6D3qn/oAFcwpj9vcGfRjQWjO
39
- ARzXM2vUWEet4OVn3DXyOdaWFR1ppehz7rAWBiPgsMg4fjAusYb9Mft1GMxMzuwT
40
- Oyqsp6pzAWFrCD3JAoTLxClV+j5m+SXZ/ItD6ziGpl/h7DyayrFZ
41
- -----END RSA PRIVATE KEY-----`
42
13
 
43
14
  Test('ParticipantEndpoint Model Test', modelTest => {
44
15
  let sandbox
@@ -483,10 +454,9 @@ Test('ParticipantEndpoint Model Test', modelTest => {
483
454
  method: 'post',
484
455
  headers: Helper.defaultHeaders(fsp, Enum.Http.HeaderResources.PARTICIPANTS, payeefsp)
485
456
  }
486
- const jwsSigner = new JwsSigner({
487
- logger: null,
488
- signingKey
489
- })
457
+ const jwsSigner = {
458
+ getSignature: () => 'mock-jws-signature'
459
+ }
490
460
  request = sandbox.stub().returns({ status: 200 })
491
461
  Model = proxyquire('../../../src/util/request', { axios: request })
492
462
  const signSpy = Sinon.spy(jwsSigner, 'getSignature')
@@ -517,10 +487,9 @@ Test('ParticipantEndpoint Model Test', modelTest => {
517
487
  method: 'post',
518
488
  headers: Helper.defaultHeaders(fsp, Enum.Http.HeaderResources.PARTICIPANTS, payeefsp)
519
489
  }
520
- const jwsSigner = new JwsSigner({
521
- logger: null,
522
- signingKey
523
- })
490
+ const jwsSigner = {
491
+ getSignature: () => 'mock-jws-signature'
492
+ }
524
493
  request = sandbox.stub().returns({ status: 200 })
525
494
  Model = proxyquire('../../../src/util/request', { axios: request })
526
495
  const signSpy = Sinon.spy(jwsSigner, 'getSignature')
@@ -1,75 +0,0 @@
1
- /*****
2
- License
3
- --------------
4
- Copyright © 2020-2025 Mojaloop Foundation
5
- The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
-
7
- http://www.apache.org/licenses/LICENSE-2.0
8
-
9
- Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
-
11
- Contributors
12
- --------------
13
- This is the official list of the Mojaloop project contributors for this file.
14
- Names of the original copyright holders (individuals or organizations)
15
- should be listed with a '*' in the first column. People who have
16
- contributed from an organization can be listed under the organization
17
- that actually holds the copyright for their contributions (see the
18
- Mojaloop Foundation for an example). Those individuals should have
19
- their names indented and be marked with a '-'. Email address can be added
20
- optionally within square brackets <email>.
21
-
22
- * Mojaloop Foundation
23
- * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
-
25
- --------------
26
- ******/
27
- /* istanbul ignore file */
28
-
29
- const otel = require('@opentelemetry/semantic-conventions')
30
-
31
- const ATTR_SERVICE_PEER_NAME = 'service.peer.name' // using string literal because ATTR_SERVICE_PEER_NAME is only available in @opentelemetry/semantic-conventions/incubating as of now
32
- const CUSTOM_REQUEST_ID = 'request.id'
33
-
34
- /** @typedef { attributes: Record<string, any> } OTelAttributes */
35
-
36
- /** @returns OTelAttributes */
37
- const outgoingRequestAttributesDto = ({
38
- method, url, durationSec, statusCode, errorType, peerService
39
- }) => ({
40
- attributes: {
41
- [otel.ATTR_HTTP_REQUEST_METHOD]: method,
42
- [otel.ATTR_URL_FULL]: url,
43
- [otel.METRIC_HTTP_CLIENT_REQUEST_DURATION]: durationSec, // 'duration.ms' is a custom attribute
44
- ...(statusCode && { [otel.ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode }),
45
- ...(errorType && { [otel.ATTR_ERROR_TYPE]: errorType }),
46
- ...(peerService && { [ATTR_SERVICE_PEER_NAME]: peerService })
47
- // peerService - logical service name, must be explicitly provided by caller (not derived from URL hostname)
48
- // think if we should extract it for internal http://... calls from url hostname
49
- }
50
- })
51
-
52
- /** @returns OTelAttributes */
53
- const incomingRequestAttributesDto = ({
54
- method, url, path, route,
55
- serverAddress, clientAddress, userAgent, durationSec, requestId, statusCode, errorType
56
- }) => ({
57
- attributes: {
58
- [otel.ATTR_HTTP_REQUEST_METHOD]: method,
59
- [otel.ATTR_URL_FULL]: url,
60
- [otel.ATTR_URL_PATH]: path,
61
- [otel.ATTR_HTTP_ROUTE]: route,
62
- [otel.ATTR_SERVER_ADDRESS]: serverAddress,
63
- [otel.ATTR_CLIENT_ADDRESS]: clientAddress,
64
- [otel.ATTR_USER_AGENT_ORIGINAL]: userAgent,
65
- [CUSTOM_REQUEST_ID]: requestId,
66
- ...(durationSec && { [otel.METRIC_HTTP_SERVER_REQUEST_DURATION]: durationSec }), // 'duration.ms' is a custom attribute
67
- ...(statusCode && { [otel.ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode }),
68
- ...(errorType && { [otel.ATTR_ERROR_TYPE]: errorType })
69
- }
70
- })
71
-
72
- module.exports = {
73
- outgoingRequestAttributesDto,
74
- incomingRequestAttributesDto
75
- }