account-lookup-service 15.5.0-iso.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +11 -0
- package/.ncurc.yaml +6 -0
- package/.nvmrc +1 -0
- package/.nycrc.yml +20 -0
- package/.versionrc +15 -0
- package/CHANGELOG.md +330 -0
- package/CODEOWNERS +38 -0
- package/Dockerfile +45 -0
- package/LICENSE.md +10 -0
- package/README.md +252 -0
- package/audit-ci.jsonc +32 -0
- package/audit-resolve.json +161 -0
- package/config/default.json +109 -0
- package/config/knexfile.js +21 -0
- package/docker/account-lookup-service/default.json +106 -0
- package/docker/account-lookup-service/make-default-json.sh +5 -0
- package/docker/account-lookup-service/override.json +15 -0
- package/docker/central-ledger/default.json +458 -0
- package/docker/config-modifier/account-lookup-service.js +31 -0
- package/docker/kafka/consumer.properties +26 -0
- package/docker/kafka/producer.properties +45 -0
- package/docker/kafka/server.properties +143 -0
- package/docker/kafka/tools-log4j.properties +21 -0
- package/docker/mock-proxy/Dockerfile +15 -0
- package/docker/mock-proxy/package-lock.json +4986 -0
- package/docker/mock-proxy/package.json +24 -0
- package/docker/mock-proxy/src/config.ts +14 -0
- package/docker/mock-proxy/src/server.ts +94 -0
- package/docker/mock-proxy/src/utils.ts +29 -0
- package/docker/mock-proxy/tsconfig.json +24 -0
- package/docker/sql-init/01_permissions.sql +2 -0
- package/docker/sql-init-central-ledger/01_permissions.sql +2 -0
- package/docker/wait-for/wait-for-account-lookup-service.sh +10 -0
- package/docker/wait-for/wait-for-central-ledger.sh +11 -0
- package/docker/wait-for/wait-for-kafka.sh +7 -0
- package/docker/wait-for/wait-for-ml-api-adapter.sh +9 -0
- package/docker/wait-for/wait-for-mockserver.sh +20 -0
- package/docker/wait-for/wait-for-mysql-als.sh +14 -0
- package/docker/wait-for/wait-for-mysql-central-ledger.sh +11 -0
- package/docker/wait-for/wait-for-mysql.sh +11 -0
- package/docker/wait-for/wait-for-objstore.sh +12 -0
- package/docker/wait-for/wait-for.env +18 -0
- package/docker/wait-for/wait-for.sh +81 -0
- package/docker-compose.integration.yml +29 -0
- package/docker-compose.yml +243 -0
- package/jest-int.config.js +8 -0
- package/jest.config.js +16 -0
- package/jsdoc.json +38 -0
- package/migrations/01_currency.js +42 -0
- package/migrations/02_endpointType.js +43 -0
- package/migrations/03_endpointType-indexes.js +37 -0
- package/migrations/04_partyIdType.js +43 -0
- package/migrations/05_partyIdType-indexes.js +38 -0
- package/migrations/08_oracleEndpoint.js +51 -0
- package/migrations/09_oracleEndpoint-indexes.js +41 -0
- package/migrations/10_oracleEndpoint-remove-constraints.js +38 -0
- package/package.json +180 -0
- package/scripts/_wait4_all.js +143 -0
- package/scripts/test-functional.sh +76 -0
- package/secrets/jwsSigningKey.key +27 -0
- package/seeds/currency.js +765 -0
- package/seeds/endpointType.js +65 -0
- package/seeds/partyIdType.js +79 -0
- package/src/api/endpointcache.js +67 -0
- package/src/api/health.js +66 -0
- package/src/api/index.js +85 -0
- package/src/api/oracles/{ID}.js +100 -0
- package/src/api/oracles.js +96 -0
- package/src/api/participants/{ID}/error.js +44 -0
- package/src/api/participants/{ID}.js +44 -0
- package/src/api/participants/{Type}/{ID}/error.js +74 -0
- package/src/api/participants/{Type}/{ID}/{SubId}/error.js +68 -0
- package/src/api/participants/{Type}/{ID}/{SubId}.js +113 -0
- package/src/api/participants/{Type}/{ID}.js +133 -0
- package/src/api/participants.js +63 -0
- package/src/api/parties/{Type}/{ID}/error.js +66 -0
- package/src/api/parties/{Type}/{ID}/{SubId}/error.js +56 -0
- package/src/api/parties/{Type}/{ID}/{SubId}.js +77 -0
- package/src/api/parties/{Type}/{ID}.js +98 -0
- package/src/api/routes.js +294 -0
- package/src/constants.js +16 -0
- package/src/domain/oracle/index.js +33 -0
- package/src/domain/oracle/oracle.js +234 -0
- package/src/domain/participants/index.js +35 -0
- package/src/domain/participants/participants.js +560 -0
- package/src/domain/parties/getPartiesByTypeAndID.js +239 -0
- package/src/domain/parties/index.js +32 -0
- package/src/domain/parties/parties.js +215 -0
- package/src/domain/parties/utils.js +84 -0
- package/src/domain/timeout/dto.js +48 -0
- package/src/domain/timeout/index.js +104 -0
- package/src/handlers/TimeoutHandler.js +94 -0
- package/src/handlers/index.js +70 -0
- package/src/handlers/monitoring/index.js +51 -0
- package/src/handlers/monitoring/plugins/health.js +61 -0
- package/src/handlers/monitoring/plugins/metrics.js +48 -0
- package/src/handlers/register.js +102 -0
- package/src/index.js +66 -0
- package/src/interface/admin-swagger.yaml +804 -0
- package/src/interface/admin_swagger.json +959 -0
- package/src/interface/api-swagger-iso20022-parties.yaml +1734 -0
- package/src/interface/api-swagger.yaml +1733 -0
- package/src/interface/api_swagger.json +3046 -0
- package/src/interface/fspiop-rest-v2.0-ISO20022_parties.yaml +2256 -0
- package/src/interface/thirdparty/admin-swagger.yaml +808 -0
- package/src/interface/thirdparty/admin_swagger.json +961 -0
- package/src/interface/thirdparty/api-swagger.yaml +1739 -0
- package/src/interface/thirdparty/api_swagger.json +3142 -0
- package/src/lib/argv.js +39 -0
- package/src/lib/cache.js +126 -0
- package/src/lib/config.js +183 -0
- package/src/lib/db.js +26 -0
- package/src/lib/headers.js +53 -0
- package/src/lib/healthCheck/subServiceHealth.js +84 -0
- package/src/lib/index.js +11 -0
- package/src/lib/migrator.js +17 -0
- package/src/lib/requestLogger.js +54 -0
- package/src/lib/util.js +66 -0
- package/src/metrics/handler.js +33 -0
- package/src/metrics/plugin.js +52 -0
- package/src/metrics/routes.js +43 -0
- package/src/models/currency/currency.js +48 -0
- package/src/models/currency/index.js +32 -0
- package/src/models/endpointType/endpointType.js +48 -0
- package/src/models/endpointType/index.js +32 -0
- package/src/models/misc/migrationLock.js +49 -0
- package/src/models/oracle/facade.js +341 -0
- package/src/models/oracle/index.js +41 -0
- package/src/models/oracle/oracleEndpoint.js +192 -0
- package/src/models/oracle/oracleEndpointCached.js +108 -0
- package/src/models/participantEndpoint/facade.js +238 -0
- package/src/models/partyIdType/index.js +32 -0
- package/src/models/partyIdType/partyIdType.js +41 -0
- package/src/plugins.js +139 -0
- package/src/server.js +199 -0
- package/test/fixtures/index.js +131 -0
- package/test/fixtures/iso.js +110 -0
- package/test/integration/.env +8 -0
- package/test/integration/api/parties.test.js +137 -0
- package/test/integration/constants.js +20 -0
- package/test/integration/domain/oracle/index.test.js +324 -0
- package/test/integration/domain/timeout/index.test.js +75 -0
- package/test/integration/env.sh +15 -0
- package/test/integration/example.test.js +12 -0
- package/test/integration/models/currency/currency.test.js +68 -0
- package/test/integration/plugins.test.js +62 -0
- package/test/integration/prepareTestParticipants.js +30 -0
- package/test/integration/setup.js +5 -0
- package/test/integration-config.json +81 -0
- package/test/integration-runner.sh +108 -0
- package/test/unit/api/health.test.js +142 -0
- package/test/unit/api/oracles/{ID}.test.js +264 -0
- package/test/unit/api/oracles.test.js +173 -0
- package/test/unit/api/participants/participants.test.js +117 -0
- package/test/unit/api/participants/{Type}/{ID}/error.test.js +155 -0
- package/test/unit/api/participants/{Type}/{ID}/{SubId}/error.test.js +131 -0
- package/test/unit/api/participants/{Type}/{ID}/{SubId}.test.js +377 -0
- package/test/unit/api/participants/{Type}/{ID}.test.js +383 -0
- package/test/unit/api/participants.test.js +108 -0
- package/test/unit/api/parties/endpointcache.test.js +83 -0
- package/test/unit/api/parties/parties.test.js +102 -0
- package/test/unit/api/parties/{Type}/{ID}/error.test.js +145 -0
- package/test/unit/api/parties/{Type}/{ID}/{SubId}/error.test.js +141 -0
- package/test/unit/api/parties/{Type}/{ID}/{SubId}.test.js +241 -0
- package/test/unit/api/parties/{Type}/{ID}.test.js +240 -0
- package/test/unit/domain/oracle/oracle.test.js +505 -0
- package/test/unit/domain/participants/participants.test.js +1724 -0
- package/test/unit/domain/parties/parties.test.js +940 -0
- package/test/unit/domain/timeout/dto.test.js +28 -0
- package/test/unit/domain/timeout/index.test.js +81 -0
- package/test/unit/handlers/TimeoutHandler.test.js +125 -0
- package/test/unit/handlers/index.test.js +56 -0
- package/test/unit/handlers/register.test.js +90 -0
- package/test/unit/index.test.js +139 -0
- package/test/unit/iso20022/partiesValidation.test.js +129 -0
- package/test/unit/lib/TransformFacades.test.js +18 -0
- package/test/unit/lib/argv.test.js +40 -0
- package/test/unit/lib/cache.test.js +172 -0
- package/test/unit/lib/config.test.js +108 -0
- package/test/unit/lib/healthCheck/subServiceHealth.test.js +89 -0
- package/test/unit/lib/migrator.test.js +52 -0
- package/test/unit/lib/requestLogger.test.js +115 -0
- package/test/unit/lib/util.test.js +68 -0
- package/test/unit/mocks.js +66 -0
- package/test/unit/models/currency/currency.test.js +91 -0
- package/test/unit/models/endpointType/endpointType.test.js +69 -0
- package/test/unit/models/misc/migrationLock.test.js +96 -0
- package/test/unit/models/oracle/facade.test.js +546 -0
- package/test/unit/models/oracle/oracleEndpoint.test.js +409 -0
- package/test/unit/models/oracle/oracleEndpointCached.test.js +153 -0
- package/test/unit/models/participantEndpoint/facade.test.js +295 -0
- package/test/unit/models/partyIdType/partyIdType.test.js +88 -0
- package/test/unit/plugins.test.js +89 -0
- package/test/unit/setup.js +7 -0
- package/test/util/apiClients/AlsApiClient.js +44 -0
- package/test/util/apiClients/BasicApiClient.js +34 -0
- package/test/util/apiClients/ProxyApiClient.js +25 -0
- package/test/util/apiClients/index.js +7 -0
- package/test/util/helper.js +332 -0
- package/test/util/index.js +11 -0
- package/test/util/mockgen.js +43 -0
- package/test/util/onboarding.js +132 -0
- package/test/util/scripts/addAlsDb.sh +33 -0
- package/test/util/scripts/configureMockServer.sh +35 -0
- package/test/util/scripts/env.sh +19 -0
- package/test/util/scripts/populateTestData.sh +62 -0
- package/test/util/scripts/startMockCentralServer.sh +45 -0
- package/test/util/scripts/startMockOracleServer.sh +45 -0
- package/test/util/testConfig.js +44 -0
package/src/lib/argv.js
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
/*****
|
2
|
+
License
|
3
|
+
--------------
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
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.
|
8
|
+
Contributors
|
9
|
+
--------------
|
10
|
+
This is the official list of the Mojaloop project contributors for this file.
|
11
|
+
Names of the original copyright holders (individuals or organizations)
|
12
|
+
should be listed with a '*' in the first column. People who have
|
13
|
+
contributed from an organization can be listed under the organization
|
14
|
+
that actually holds the copyright for their contributions (see the
|
15
|
+
Gates Foundation organization for an example). Those individuals should have
|
16
|
+
their names indented and be marked with a '-'. Email address can be added
|
17
|
+
optionally within square brackets <email>.
|
18
|
+
* Gates Foundation
|
19
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
20
|
+
|
21
|
+
* Crosslake
|
22
|
+
- Lewis daly <lewisd@crosslaketech.com>
|
23
|
+
--------------
|
24
|
+
******/
|
25
|
+
|
26
|
+
/**
|
27
|
+
* @name getArgs
|
28
|
+
*
|
29
|
+
* @description Provide a mockable way to override the process.argv
|
30
|
+
*
|
31
|
+
* @returns {Array<String>} - A list of the process args
|
32
|
+
*/
|
33
|
+
const getArgs = () => {
|
34
|
+
return process.argv
|
35
|
+
}
|
36
|
+
|
37
|
+
module.exports = {
|
38
|
+
getArgs
|
39
|
+
}
|
package/src/lib/cache.js
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
/*****
|
2
|
+
License
|
3
|
+
--------------
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
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.
|
8
|
+
Contributors
|
9
|
+
--------------
|
10
|
+
This is the official list of the Mojaloop project contributors for this file.
|
11
|
+
Names of the original copyright holders (individuals or organizations)
|
12
|
+
should be listed with a '*' in the first column. People who have
|
13
|
+
contributed from an organization can be listed under the organization
|
14
|
+
that actually holds the copyright for their contributions (see the
|
15
|
+
Gates Foundation organization for an example). Those individuals should have
|
16
|
+
their names indented and be marked with a '-'. Email address can be added
|
17
|
+
optionally within square brackets <email>.
|
18
|
+
* Gates Foundation
|
19
|
+
* Name Surname <name.surname@gatesfoundation.com>
|
20
|
+
|
21
|
+
* Kevin Leyow <kevin.leyow@infitx.com>
|
22
|
+
|
23
|
+
--------------
|
24
|
+
******/
|
25
|
+
|
26
|
+
'use strict'
|
27
|
+
|
28
|
+
const CatboxMemory = require('@hapi/catbox-memory')
|
29
|
+
const Config = require('../lib/config')
|
30
|
+
|
31
|
+
let enabled = true
|
32
|
+
let ttl
|
33
|
+
let catboxMemoryClient = null
|
34
|
+
|
35
|
+
class CacheClient {
|
36
|
+
constructor (meta) {
|
37
|
+
this.meta = meta
|
38
|
+
}
|
39
|
+
|
40
|
+
getMeta () {
|
41
|
+
return this.meta
|
42
|
+
}
|
43
|
+
|
44
|
+
createKey (id) {
|
45
|
+
return {
|
46
|
+
segment: this.meta.id,
|
47
|
+
id
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
get (key) {
|
52
|
+
if (enabled) {
|
53
|
+
return catboxMemoryClient.get(key)
|
54
|
+
}
|
55
|
+
return null
|
56
|
+
}
|
57
|
+
|
58
|
+
set (key, value) {
|
59
|
+
catboxMemoryClient.set(key, value, parseInt(ttl))
|
60
|
+
}
|
61
|
+
|
62
|
+
drop (key) {
|
63
|
+
catboxMemoryClient.drop(key)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
/*
|
68
|
+
Each client should register itself during module load.
|
69
|
+
The client meta should be:
|
70
|
+
{
|
71
|
+
id [MANDATORY]
|
72
|
+
preloadCache() [OPTIONAL]
|
73
|
+
this will be called to preload data
|
74
|
+
}
|
75
|
+
*/
|
76
|
+
let cacheClients = {}
|
77
|
+
|
78
|
+
const registerCacheClient = (clientMeta) => {
|
79
|
+
const newClient = new CacheClient(clientMeta)
|
80
|
+
cacheClients[clientMeta.id] = newClient
|
81
|
+
return newClient
|
82
|
+
}
|
83
|
+
|
84
|
+
const initCache = async function () {
|
85
|
+
// Read config
|
86
|
+
ttl = Config.GENERAL_CACHE_CONFIG.EXPIRES_IN_MS
|
87
|
+
enabled = Config.GENERAL_CACHE_CONFIG.CACHE_ENABLED
|
88
|
+
|
89
|
+
// Init catbox.
|
90
|
+
catboxMemoryClient = new CatboxMemory.Engine({
|
91
|
+
maxByteSize: Config.GENERAL_CACHE_CONFIG.MAX_BYTE_SIZE
|
92
|
+
})
|
93
|
+
await catboxMemoryClient.start()
|
94
|
+
|
95
|
+
for (const clientId in cacheClients) {
|
96
|
+
const clientMeta = cacheClients[clientId].getMeta()
|
97
|
+
await clientMeta.preloadCache()
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
const destroyCache = async function () {
|
102
|
+
await catboxMemoryClient.stop()
|
103
|
+
catboxMemoryClient = null
|
104
|
+
}
|
105
|
+
|
106
|
+
const dropClients = function () {
|
107
|
+
cacheClients = {}
|
108
|
+
}
|
109
|
+
|
110
|
+
const isCacheEnabled = function () {
|
111
|
+
return enabled
|
112
|
+
}
|
113
|
+
|
114
|
+
module.exports = {
|
115
|
+
// Clients registration
|
116
|
+
registerCacheClient,
|
117
|
+
|
118
|
+
// Init & destroy the cache
|
119
|
+
initCache,
|
120
|
+
destroyCache,
|
121
|
+
isCacheEnabled,
|
122
|
+
|
123
|
+
// exposed for tests
|
124
|
+
CatboxMemory,
|
125
|
+
dropClients
|
126
|
+
}
|
@@ -0,0 +1,183 @@
|
|
1
|
+
/*****
|
2
|
+
* @file This registers all handlers for the central-ledger API
|
3
|
+
License
|
4
|
+
--------------
|
5
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
6
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
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.
|
11
|
+
|
12
|
+
Contributors
|
13
|
+
--------------
|
14
|
+
This is the official list of the Mojaloop project contributors for this file.
|
15
|
+
Names of the original copyright holders (individuals or organizations)
|
16
|
+
should be listed with a '*' in the first column. People who have
|
17
|
+
contributed from an organization can be listed under the organization
|
18
|
+
that actually holds the copyright for their contributions (see the
|
19
|
+
Gates Foundation organization for an example). Those individuals should have
|
20
|
+
their names indented and be marked with a '-'. Email address can be added
|
21
|
+
optionally within square brackets <email>.
|
22
|
+
|
23
|
+
* Gates Foundation
|
24
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
25
|
+
|
26
|
+
* Rajiv Mothilal <rajiv.mothilal@modusbox.com>
|
27
|
+
|
28
|
+
--------------
|
29
|
+
******/
|
30
|
+
const fs = require('node:fs')
|
31
|
+
const RC = require('parse-strings-in-object')(require('rc')('ALS', require('../../config/default.json')))
|
32
|
+
const { storageTypeValues } = require('@mojaloop/inter-scheme-proxy-cache-lib')
|
33
|
+
|
34
|
+
function getFileContent (path) {
|
35
|
+
if (!fs.existsSync(path)) {
|
36
|
+
console.log(`File ${path} doesn't exist, can't enable JWS signing`)
|
37
|
+
throw new Error('File doesn\'t exist')
|
38
|
+
}
|
39
|
+
return fs.readFileSync(path)
|
40
|
+
}
|
41
|
+
|
42
|
+
const getOrDefault = (value, defaultValue) => {
|
43
|
+
if (value === undefined) {
|
44
|
+
return defaultValue
|
45
|
+
}
|
46
|
+
|
47
|
+
return value
|
48
|
+
}
|
49
|
+
|
50
|
+
const DEFAULT_PROTOCOL_VERSION = {
|
51
|
+
CONTENT: {
|
52
|
+
DEFAULT: '1.1',
|
53
|
+
VALIDATELIST: [
|
54
|
+
'1.0',
|
55
|
+
'1.1'
|
56
|
+
]
|
57
|
+
},
|
58
|
+
ACCEPT: {
|
59
|
+
DEFAULT: '1',
|
60
|
+
VALIDATELIST: [
|
61
|
+
'1',
|
62
|
+
'1.0',
|
63
|
+
'1.1'
|
64
|
+
]
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
const getProtocolVersions = (defaultProtocolVersions, overrideProtocolVersions) => {
|
69
|
+
const T_PROTOCOL_VERSION = {
|
70
|
+
...defaultProtocolVersions,
|
71
|
+
...overrideProtocolVersions
|
72
|
+
}
|
73
|
+
|
74
|
+
if (overrideProtocolVersions && overrideProtocolVersions.CONTENT) {
|
75
|
+
T_PROTOCOL_VERSION.CONTENT = {
|
76
|
+
...defaultProtocolVersions.CONTENT,
|
77
|
+
...overrideProtocolVersions.CONTENT
|
78
|
+
}
|
79
|
+
}
|
80
|
+
if (overrideProtocolVersions && overrideProtocolVersions.ACCEPT) {
|
81
|
+
T_PROTOCOL_VERSION.ACCEPT = {
|
82
|
+
...defaultProtocolVersions.ACCEPT,
|
83
|
+
...overrideProtocolVersions.ACCEPT
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
if (T_PROTOCOL_VERSION.CONTENT &&
|
88
|
+
T_PROTOCOL_VERSION.CONTENT.VALIDATELIST &&
|
89
|
+
(typeof T_PROTOCOL_VERSION.CONTENT.VALIDATELIST === 'string' ||
|
90
|
+
T_PROTOCOL_VERSION.CONTENT.VALIDATELIST instanceof String)) {
|
91
|
+
T_PROTOCOL_VERSION.CONTENT.VALIDATELIST = JSON.parse(T_PROTOCOL_VERSION.CONTENT.VALIDATELIST)
|
92
|
+
}
|
93
|
+
if (T_PROTOCOL_VERSION.ACCEPT &&
|
94
|
+
T_PROTOCOL_VERSION.ACCEPT.VALIDATELIST &&
|
95
|
+
(typeof T_PROTOCOL_VERSION.ACCEPT.VALIDATELIST === 'string' ||
|
96
|
+
T_PROTOCOL_VERSION.ACCEPT.VALIDATELIST instanceof String)) {
|
97
|
+
T_PROTOCOL_VERSION.ACCEPT.VALIDATELIST = JSON.parse(T_PROTOCOL_VERSION.ACCEPT.VALIDATELIST)
|
98
|
+
}
|
99
|
+
return T_PROTOCOL_VERSION
|
100
|
+
}
|
101
|
+
|
102
|
+
if (RC.PROXY_CACHE?.enabled && !storageTypeValues.includes(RC.PROXY_CACHE.type)) {
|
103
|
+
throw new TypeError(`Incorrect proxyCache type: ${RC.PROXY_CACHE.type}`)
|
104
|
+
}
|
105
|
+
|
106
|
+
const config = {
|
107
|
+
HUB_ID: RC.HUB_PARTICIPANT.ID,
|
108
|
+
HUB_NAME: RC.HUB_PARTICIPANT.NAME,
|
109
|
+
API_PORT: RC.API_PORT,
|
110
|
+
API_TYPE: RC.API_TYPE, // 'fspiop' or 'iso20022'
|
111
|
+
DATABASE: {
|
112
|
+
client: RC.DATABASE.DIALECT,
|
113
|
+
connection: {
|
114
|
+
host: RC.DATABASE.HOST.replace(/\/$/, ''),
|
115
|
+
port: RC.DATABASE.PORT,
|
116
|
+
user: RC.DATABASE.USER,
|
117
|
+
password: RC.DATABASE.PASSWORD,
|
118
|
+
database: RC.DATABASE.DATABASE
|
119
|
+
},
|
120
|
+
pool: {
|
121
|
+
// minimum size
|
122
|
+
min: getOrDefault(RC.DATABASE.POOL_MIN_SIZE, 2),
|
123
|
+
|
124
|
+
// maximum size
|
125
|
+
max: getOrDefault(RC.DATABASE.POOL_MAX_SIZE, 10),
|
126
|
+
// acquire promises are rejected after this many milliseconds
|
127
|
+
// if a resource cannot be acquired
|
128
|
+
acquireTimeoutMillis: getOrDefault(RC.DATABASE.ACQUIRE_TIMEOUT_MILLIS, 30000),
|
129
|
+
|
130
|
+
// create operations are cancelled after this many milliseconds
|
131
|
+
// if a resource cannot be acquired
|
132
|
+
createTimeoutMillis: getOrDefault(RC.DATABASE.CREATE_TIMEOUT_MILLIS, 3000),
|
133
|
+
|
134
|
+
// destroy operations are awaited for at most this many milliseconds
|
135
|
+
// new resources will be created after this timeout
|
136
|
+
destroyTimeoutMillis: getOrDefault(RC.DATABASE.DESTROY_TIMEOUT_MILLIS, 5000),
|
137
|
+
|
138
|
+
// free resouces are destroyed after this many milliseconds
|
139
|
+
idleTimeoutMillis: getOrDefault(RC.DATABASE.IDLE_TIMEOUT_MILLIS, 30000),
|
140
|
+
|
141
|
+
// how often to check for idle resources to destroy
|
142
|
+
reapIntervalMillis: getOrDefault(RC.DATABASE.REAP_INTERVAL_MILLIS, 1000),
|
143
|
+
|
144
|
+
// long long to idle after failed create before trying again
|
145
|
+
createRetryIntervalMillis: getOrDefault(RC.DATABASE.CREATE_RETRY_INTERVAL_MILLIS, 20)
|
146
|
+
// ping: function (conn, cb) { conn.query('SELECT 1', cb) }
|
147
|
+
},
|
148
|
+
debug: getOrDefault(RC.DATABASE.DEBUG, false)
|
149
|
+
},
|
150
|
+
DISPLAY_ROUTES: RC.DISPLAY_ROUTES,
|
151
|
+
RUN_MIGRATIONS: RC.RUN_MIGRATIONS,
|
152
|
+
ADMIN_PORT: RC.ADMIN_PORT,
|
153
|
+
CENTRAL_SHARED_ENDPOINT_CACHE_CONFIG: RC.CENTRAL_SHARED_ENDPOINT_CACHE_CONFIG,
|
154
|
+
CENTRAL_SHARED_PARTICIPANT_CACHE_CONFIG: RC.CENTRAL_SHARED_PARTICIPANT_CACHE_CONFIG,
|
155
|
+
GENERAL_CACHE_CONFIG: RC.GENERAL_CACHE_CONFIG,
|
156
|
+
HANDLERS: RC.HANDLERS,
|
157
|
+
HANDLERS_DISABLED: RC.HANDLERS.DISABLED,
|
158
|
+
HANDLERS_MONITORING_PORT: RC.HANDLERS.MONITORING_PORT,
|
159
|
+
HANDLERS_TIMEOUT: RC.HANDLERS.TIMEOUT,
|
160
|
+
HANDLERS_TIMEOUT_DISABLED: RC.HANDLERS.TIMEOUT.DISABLED,
|
161
|
+
HANDLERS_TIMEOUT_TIMEXP: RC.HANDLERS.TIMEOUT.TIMEXP,
|
162
|
+
HANDLERS_TIMEOUT_TIMEZONE: RC.HANDLERS.TIMEOUT.TIMEZONE,
|
163
|
+
HANDLERS_TIMEOUT_BATCH_SIZE: RC.HANDLERS.TIMEOUT.BATCH_SIZE,
|
164
|
+
ERROR_HANDLING: RC.ERROR_HANDLING,
|
165
|
+
SWITCH_ENDPOINT: RC.SWITCH_ENDPOINT,
|
166
|
+
INSTRUMENTATION_METRICS_DISABLED: RC.INSTRUMENTATION.METRICS.DISABLED,
|
167
|
+
INSTRUMENTATION_METRICS_LABELS: RC.INSTRUMENTATION.METRICS.labels,
|
168
|
+
INSTRUMENTATION_METRICS_CONFIG: RC.INSTRUMENTATION.METRICS.config,
|
169
|
+
JWS_SIGN: RC.ENDPOINT_SECURITY.JWS.JWS_SIGN,
|
170
|
+
FSPIOP_SOURCE_TO_SIGN: RC.HUB_NAME,
|
171
|
+
JWS_SIGNING_KEY_PATH: RC.ENDPOINT_SECURITY.JWS.JWS_SIGNING_KEY_PATH,
|
172
|
+
API_DOC_ENDPOINTS_ENABLED: RC.API_DOC_ENDPOINTS_ENABLED || false,
|
173
|
+
FEATURE_ENABLE_EXTENDED_PARTY_ID_TYPE: RC.FEATURE_ENABLE_EXTENDED_PARTY_ID_TYPE || false,
|
174
|
+
PROTOCOL_VERSIONS: getProtocolVersions(DEFAULT_PROTOCOL_VERSION, RC.PROTOCOL_VERSIONS),
|
175
|
+
PROXY_CACHE_CONFIG: RC.PROXY_CACHE,
|
176
|
+
proxyMap: RC.PROXY_MAP
|
177
|
+
}
|
178
|
+
|
179
|
+
if (config.JWS_SIGN) {
|
180
|
+
config.JWS_SIGNING_KEY = getFileContent(config.JWS_SIGNING_KEY_PATH)
|
181
|
+
}
|
182
|
+
|
183
|
+
module.exports = config
|
package/src/lib/db.js
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
/*****
|
2
|
+
License
|
3
|
+
--------------
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
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.
|
8
|
+
Contributors
|
9
|
+
--------------
|
10
|
+
This is the official list of the Mojaloop project contributors for this file.
|
11
|
+
Names of the original copyright holders (individuals or organizations)
|
12
|
+
should be listed with a '*' in the first column. People who have
|
13
|
+
contributed from an organization can be listed under the organization
|
14
|
+
that actually holds the copyright for their contributions (see the
|
15
|
+
Gates Foundation organization for an example). Those individuals should have
|
16
|
+
their names indented and be marked with a '-'. Email address can be added
|
17
|
+
optionally within square brackets <email>.
|
18
|
+
* Gates Foundation
|
19
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
20
|
+
|
21
|
+
* Rajiv Mothilal <rajiv.mothilal@modusbox.com>
|
22
|
+
--------------
|
23
|
+
******/
|
24
|
+
'use strict'
|
25
|
+
|
26
|
+
module.exports = require('@mojaloop/database-lib').Db
|
@@ -0,0 +1,53 @@
|
|
1
|
+
/*****
|
2
|
+
License
|
3
|
+
--------------
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
Gates Foundation organization 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
|
+
* Gates Foundation
|
23
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
24
|
+
|
25
|
+
* Steven Oderayi <steven.oderayi@modusbox.com>
|
26
|
+
|
27
|
+
--------------
|
28
|
+
******/
|
29
|
+
'use strict'
|
30
|
+
|
31
|
+
const Mustache = require('mustache')
|
32
|
+
const Enums = require('@mojaloop/central-services-shared').Enum
|
33
|
+
const Config = require('../lib/config')
|
34
|
+
|
35
|
+
/**
|
36
|
+
* @function createErrorCallbackHeaders
|
37
|
+
* @description it returns the FSPIOP headers for error callback
|
38
|
+
* @param {object} params - parameters to the function with the shape `{ requestHeaders, partyIdType, partyIdentifier, endpointTemplate }`
|
39
|
+
*
|
40
|
+
* @returns {object} - FSPIOP callback headers merged with the request headers passed in `params.requestHeaders`
|
41
|
+
*/
|
42
|
+
exports.createCallbackHeaders = (params) => {
|
43
|
+
const callbackHeaders = { ...params.requestHeaders }
|
44
|
+
|
45
|
+
callbackHeaders[Enums.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
|
46
|
+
callbackHeaders[Enums.Http.Headers.FSPIOP.DESTINATION] = params.requestHeaders[Enums.Http.Headers.FSPIOP.SOURCE]
|
47
|
+
callbackHeaders[Enums.Http.Headers.FSPIOP.HTTP_METHOD] = Enums.Http.RestMethods.PUT
|
48
|
+
callbackHeaders[Enums.Http.Headers.FSPIOP.URI] = Mustache.render(params.endpointTemplate, {
|
49
|
+
partyIdType: params.partyIdType, partyIdentifier: params.partyIdentifier
|
50
|
+
})
|
51
|
+
|
52
|
+
return callbackHeaders
|
53
|
+
}
|
@@ -0,0 +1,84 @@
|
|
1
|
+
/*****
|
2
|
+
License
|
3
|
+
--------------
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
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.
|
8
|
+
Contributors
|
9
|
+
--------------
|
10
|
+
This is the official list of the Mojaloop project contributors for this file.
|
11
|
+
Names of the original copyright holders (individuals or organizations)
|
12
|
+
should be listed with a '*' in the first column. People who have
|
13
|
+
contributed from an organization can be listed under the organization
|
14
|
+
that actually holds the copyright for their contributions (see the
|
15
|
+
Gates Foundation organization for an example). Those individuals should have
|
16
|
+
their names indented and be marked with a '-'. Email address can be added
|
17
|
+
optionally within square brackets <email>.
|
18
|
+
* Gates Foundation
|
19
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
20
|
+
|
21
|
+
* Lewis Daly <lewis@vesselstech.com>
|
22
|
+
--------------
|
23
|
+
******/
|
24
|
+
'use strict'
|
25
|
+
|
26
|
+
const { statusEnum, serviceName } = require('@mojaloop/central-services-shared').HealthCheck.HealthCheckEnums
|
27
|
+
const Logger = require('@mojaloop/central-services-logger')
|
28
|
+
|
29
|
+
const MigrationLockModel = require('../../models/misc/migrationLock')
|
30
|
+
|
31
|
+
/**
|
32
|
+
* @function getSubServiceHealthDatastore
|
33
|
+
*
|
34
|
+
* @description Gets the health of the Datastore by ensuring the table is currently locked
|
35
|
+
* in a migration state. This implicity checks the connection with the database.
|
36
|
+
*
|
37
|
+
* @returns Promise<SubServiceHealth> The SubService health object for the datastore
|
38
|
+
*/
|
39
|
+
const getSubServiceHealthDatastore = async () => {
|
40
|
+
let status = statusEnum.OK
|
41
|
+
|
42
|
+
try {
|
43
|
+
const isLocked = await MigrationLockModel.getIsMigrationLocked()
|
44
|
+
if (isLocked) {
|
45
|
+
status = statusEnum.DOWN
|
46
|
+
}
|
47
|
+
} catch (err) {
|
48
|
+
Logger.isDebugEnabled && Logger.debug(`getSubServiceHealthDatastore failed with error ${err.message}.`)
|
49
|
+
status = statusEnum.DOWN
|
50
|
+
}
|
51
|
+
|
52
|
+
return {
|
53
|
+
name: serviceName.datastore,
|
54
|
+
status
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* @function getProxyCacheHealth
|
60
|
+
*
|
61
|
+
* @description Gets the health of the proxy cache by checking the connection to the cache.
|
62
|
+
*
|
63
|
+
* @returns Promise<SubServiceHealth> The SubService health object for the proxy cache
|
64
|
+
*/
|
65
|
+
const getProxyCacheHealth = async (proxyCache) => {
|
66
|
+
let status = statusEnum.OK
|
67
|
+
|
68
|
+
try {
|
69
|
+
status = await proxyCache.healthCheck() ? statusEnum.OK : statusEnum.DOWN
|
70
|
+
} catch (err) {
|
71
|
+
Logger.isDebugEnabled && Logger.debug(`getProxyCacheHealth failed with error ${err.message}.`)
|
72
|
+
status = statusEnum.DOWN
|
73
|
+
}
|
74
|
+
|
75
|
+
return {
|
76
|
+
name: serviceName.proxyCache,
|
77
|
+
status
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
module.exports = {
|
82
|
+
getSubServiceHealthDatastore,
|
83
|
+
getProxyCacheHealth
|
84
|
+
}
|
package/src/lib/index.js
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
const { loggerFactory, asyncStorage } = require('@mojaloop/central-services-logger/src/contextLogger')
|
2
|
+
const { TransformFacades } = require('@mojaloop/ml-schema-transformer-lib')
|
3
|
+
|
4
|
+
const logger = loggerFactory('ALS') // global logger without context
|
5
|
+
|
6
|
+
module.exports = {
|
7
|
+
logger,
|
8
|
+
loggerFactory,
|
9
|
+
asyncStorage,
|
10
|
+
TransformFacades
|
11
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
'use strict'
|
2
|
+
|
3
|
+
const Path = require('path')
|
4
|
+
const Migrations = require('@mojaloop/database-lib').Migrations
|
5
|
+
const Knexfile = require('../../config/knexfile')
|
6
|
+
|
7
|
+
const updateMigrationsLocation = (kf) => {
|
8
|
+
const parsedMigrationDir = Path.parse(kf.migrations.directory)
|
9
|
+
kf.migrations.directory = Path.join(process.cwd(), parsedMigrationDir.base)
|
10
|
+
const parsedSeedsDir = Path.parse(kf.seeds.directory)
|
11
|
+
kf.seeds.directory = Path.join(process.cwd(), parsedSeedsDir.base)
|
12
|
+
return kf
|
13
|
+
}
|
14
|
+
|
15
|
+
exports.migrate = async function () {
|
16
|
+
return Migrations.migrate(updateMigrationsLocation(Knexfile))
|
17
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
/*****
|
2
|
+
License
|
3
|
+
--------------
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
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.
|
8
|
+
Contributors
|
9
|
+
--------------
|
10
|
+
This is the official list of the Mojaloop project contributors for this file.
|
11
|
+
Names of the original copyright holders (individuals or organizations)
|
12
|
+
should be listed with a '*' in the first column. People who have
|
13
|
+
contributed from an organization can be listed under the organization
|
14
|
+
that actually holds the copyright for their contributions (see the
|
15
|
+
Gates Foundation organization for an example). Those individuals should have
|
16
|
+
their names indented and be marked with a '-'. Email address can be added
|
17
|
+
optionally within square brackets <email>.
|
18
|
+
* Gates Foundation
|
19
|
+
|
20
|
+
* Rajiv Mothilal <rajiv.mothilal@modusbox.com>
|
21
|
+
|
22
|
+
--------------
|
23
|
+
******/
|
24
|
+
|
25
|
+
'use strict'
|
26
|
+
|
27
|
+
const { logger, asyncStorage } = require('./index')
|
28
|
+
|
29
|
+
const logRequest = function (request) {
|
30
|
+
const { path, method, headers, payload, query } = request
|
31
|
+
const requestId = request.info.id = `${request.info.id}__${headers.traceid}`
|
32
|
+
asyncStorage.enterWith({ requestId })
|
33
|
+
|
34
|
+
logger.isInfoEnabled && logger.info(`[==> req] ${method.toUpperCase()} ${path}`, { headers, payload, query })
|
35
|
+
}
|
36
|
+
|
37
|
+
const logResponse = function (request) {
|
38
|
+
if (logger.isInfoEnabled) {
|
39
|
+
const { path, method, headers, payload, query, response } = request
|
40
|
+
const { received } = request.info
|
41
|
+
|
42
|
+
const statusCode = response instanceof Error
|
43
|
+
? response.output?.statusCode
|
44
|
+
: response.statusCode
|
45
|
+
const respTimeSec = ((Date.now() - received) / 1000).toFixed(3)
|
46
|
+
|
47
|
+
logger.info(`[<== ${statusCode}][${respTimeSec} s] ${method.toUpperCase()} ${path}`, { headers, payload, query })
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
module.exports = {
|
52
|
+
logRequest,
|
53
|
+
logResponse
|
54
|
+
}
|
package/src/lib/util.js
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
const util = require('util')
|
2
|
+
const Path = require('path')
|
3
|
+
const Enum = require('@mojaloop/central-services-shared').Enum
|
4
|
+
const { HeaderValidation, Hapi } = require('@mojaloop/central-services-shared').Util
|
5
|
+
const Config = require('../lib/config')
|
6
|
+
|
7
|
+
const getSpanTags = ({ headers }, transactionType, transactionAction) => {
|
8
|
+
const tags = {
|
9
|
+
transactionType,
|
10
|
+
transactionAction
|
11
|
+
}
|
12
|
+
if (headers && headers[Enum.Http.Headers.FSPIOP.SOURCE]) {
|
13
|
+
tags.source = headers[Enum.Http.Headers.FSPIOP.SOURCE]
|
14
|
+
}
|
15
|
+
if (headers && headers[Enum.Http.Headers.FSPIOP.DESTINATION]) {
|
16
|
+
tags.destination = headers[Enum.Http.Headers.FSPIOP.DESTINATION]
|
17
|
+
}
|
18
|
+
return tags
|
19
|
+
}
|
20
|
+
|
21
|
+
const pathForInterface = ({ isAdmin, isMockInterface }) => {
|
22
|
+
let apiFile
|
23
|
+
let pathFolder
|
24
|
+
|
25
|
+
if (Config.FEATURE_ENABLE_EXTENDED_PARTY_ID_TYPE) {
|
26
|
+
pathFolder = '../interface/thirdparty/'
|
27
|
+
} else {
|
28
|
+
pathFolder = '../interface/'
|
29
|
+
}
|
30
|
+
|
31
|
+
if (isAdmin) {
|
32
|
+
if (isMockInterface) {
|
33
|
+
apiFile = 'admin_swagger.json'
|
34
|
+
} else {
|
35
|
+
apiFile = 'admin-swagger.yaml'
|
36
|
+
}
|
37
|
+
} else {
|
38
|
+
if (isMockInterface) {
|
39
|
+
apiFile = 'api_swagger.json'
|
40
|
+
} else {
|
41
|
+
apiFile = Config.API_TYPE === Hapi.API_TYPES.iso20022
|
42
|
+
? 'api-swagger-iso20022-parties.yaml'
|
43
|
+
: 'api-swagger.yaml'
|
44
|
+
}
|
45
|
+
}
|
46
|
+
return Path.resolve(__dirname, pathFolder + apiFile)
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* @function getStackOrInspect
|
51
|
+
* @description Gets the error stack, or uses util.inspect to inspect the error
|
52
|
+
* @param {*} err - An error object
|
53
|
+
*/
|
54
|
+
function getStackOrInspect (err) {
|
55
|
+
return err?.stack || util.inspect(err)
|
56
|
+
}
|
57
|
+
|
58
|
+
module.exports = {
|
59
|
+
getSpanTags,
|
60
|
+
pathForInterface,
|
61
|
+
getStackOrInspect,
|
62
|
+
hubNameConfig: {
|
63
|
+
hubName: Config.HUB_NAME,
|
64
|
+
hubNameRegex: HeaderValidation.getHubNameRegex(Config.HUB_NAME)
|
65
|
+
}
|
66
|
+
}
|